diff --git a/Cargo.lock b/Cargo.lock index 09b33e3fe..68aaaf04d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -772,6 +782,15 @@ dependencies = [ "no_std_io2", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" @@ -1047,6 +1066,17 @@ dependencies = [ "libc", ] +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.1" @@ -1058,6 +1088,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.45" @@ -1684,6 +1727,32 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "darling" version = "0.20.11" @@ -2192,6 +2261,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "field-offset" version = "0.3.6" @@ -5506,6 +5581,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.4.1" @@ -5895,6 +5976,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polycool" version = "0.4.0" @@ -6362,7 +6454,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20", + "chacha20 0.10.1", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -7437,6 +7529,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "snow" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82" +dependencies = [ + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "getrandom 0.3.4", + "ring", + "rustc_version", + "sha2", + "subtle", +] + [[package]] name = "socket2" version = "0.6.5" @@ -8803,6 +8911,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -10555,6 +10673,18 @@ dependencies = [ "zeron-update", ] +[[package]] +name = "zeron-crypto" +version = "0.2.54" +dependencies = [ + "curve25519-dalek", + "ring", + "serde", + "serde_json", + "snow", + "zeroize", +] + [[package]] name = "zeron-doc" version = "0.2.54" @@ -10597,6 +10727,7 @@ dependencies = [ "tokio-util", "tracing", "uuid", + "zeron-crypto", "zeron-doc", "zeron-harness", "zeron-proto", @@ -10650,6 +10781,7 @@ dependencies = [ "tokio-tungstenite", "tracing", "uuid", + "zeron-crypto", "zeron-doc", "zeron-proto", "zeron-sync", @@ -10674,6 +10806,7 @@ dependencies = [ "tokio-tungstenite", "tracing", "uuid", + "zeron-crypto", "zeron-doc", "zeron-proto", ] diff --git a/Cargo.toml b/Cargo.toml index 0de181dd1..74afdd699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/crypto", "crates/theme", "crates/proto", "crates/doc", @@ -21,6 +22,7 @@ license = "MIT" publish = false [workspace.dependencies] +zeron-crypto = { path = "crates/crypto" } zeron-proto = { path = "crates/proto" } zeron-doc = { path = "crates/doc" } zeron-sync = { path = "crates/sync" } diff --git a/apps/ios/README.md b/apps/ios/README.md index 02e97a8d4..43afd64b9 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -136,3 +136,9 @@ queued row when the composer is empty. It never skips a blocked head or stops the agent merely because the draft is empty. Queue editing on iOS changes text only and preserves queued attachments, including when the text is cleared. Draft photos are hidden and the attachment picker is unavailable during editing. If the row disappears or its lease is superseded, **Copy edit and stop editing** saves the edited text to the clipboard and restores the original draft and photos. +### Encrypted physical-device testing + +The phone supports encrypted relay RPC, sealed recent-message tails, and +full tool output/diff sidecars. Follow the [Mac + iPhone test guide](../../docs/ios-encrypted-device-test.md) +for a separate local profile, installation, comparison-code pairing, and the +repeatable simulator-to-Rust live test. diff --git a/apps/ios/Zeron/App/AppConfig.swift b/apps/ios/Zeron/App/AppConfig.swift index 60964b858..91958e0f8 100644 --- a/apps/ios/Zeron/App/AppConfig.swift +++ b/apps/ios/Zeron/App/AppConfig.swift @@ -17,7 +17,31 @@ final class AppConfig: @unchecked Sendable { let deviceId: String let deviceName: String + let vault: MobileVault + let vaultPersistence: VaultPersistence + enum SyncAccess: Sendable, Equatable { case blocked, legacy, encrypted } + private var access: SyncAccess = .blocked + private var invalidated = false private let lock = NSLock() + + var syncAccess: SyncAccess { lock.withLock { access } } + func setSyncAccess(_ value: SyncAccess) { lock.withLock { access = invalidated ? .blocked : value } } + func permitsSync(encrypted: Bool) -> Bool { + lock.withLock { !invalidated && access == (encrypted ? .encrypted : .legacy) } + } + var vaultClient: MobileVaultClient { + MobileVaultClient(origin: edgeURL, orgId: orgId, token: { [weak self] in await self?.currentToken() }) + } + func invalidate() { + lock.withLock { + invalidated = true + access = .blocked + tokens = nil + devBearer = nil + refreshTask?.cancel() + refreshTask = nil + } + } private var tokens: AuthTokens? private var devBearer: String? /// In-flight refresh shared by every caller (single-flight). WorkOS @@ -40,11 +64,13 @@ final class AppConfig: @unchecked Sendable { self.deviceName = deviceName self.tokens = tokens self.devBearer = devBearer + vaultPersistence = VaultPersistence(origin: edgeURL, orgId: orgId, userId: userId) + vault = MobileVault(persistence: vaultPersistence, orgId: orgId, userId: userId) } func updateTokens(_ new: AuthTokens) { lock.withLock { - tokens = new + if !invalidated { tokens = new } } } @@ -77,9 +103,14 @@ final class AppConfig: @unchecked Sendable { let refreshed = try? await client.refresh(refreshToken: current.refreshToken, organizationId: orgId) if let refreshed { - self.updateTokens(refreshed) - Keychain.save(refreshed.accessToken, key: "accessToken") - Keychain.save(refreshed.refreshToken, key: "refreshToken") + let accepted = self.lock.withLock { + guard !self.invalidated else { return false } + self.tokens = refreshed + Keychain.save(refreshed.accessToken, key: "accessToken") + Keychain.save(refreshed.refreshToken, key: "refreshToken") + return true + } + guard accepted else { return nil } } else { roomLog.error("auth: token refresh failed; using expired access token (server will reject and rooms will redial)") } @@ -88,7 +119,7 @@ final class AppConfig: @unchecked Sendable { } // Failure falls back to the expired token: let the server // reject; the rooms' backoff redials retry through here. - return refreshed?.accessToken ?? current.accessToken + return self.lock.withLock { self.invalidated ? nil : (refreshed?.accessToken ?? current.accessToken) } } refreshTask = task return task @@ -96,6 +127,11 @@ final class AppConfig: @unchecked Sendable { return await task.value } + private func syncToken(encrypted: Bool) async -> String? { + guard permitsSync(encrypted: encrypted), let token = await currentToken(), permitsSync(encrypted: encrypted) else { return nil } + return token + } + private var wsBase: URL { var components = URLComponents(url: edgeURL, resolvingAgainstBaseURL: false)! components.scheme = components.scheme == "http" ? "ws" : "wss" @@ -104,9 +140,9 @@ final class AppConfig: @unchecked Sendable { /// The workspace registry room (docs/registry-sync.md) — the row-table /// replacement for the old ws Loro workspace doc. - func registrySocketURL() async -> URL? { - guard let token = await currentToken() else { return nil } - var url = wsBase.appending(path: "registry/\(orgId)/ws") + func registrySocketURL(encrypted: Bool = false) async -> URL? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var url = wsBase.appending(path: "registry/\(orgId)\(encrypted ? "/e1" : "")/ws") url.append(queryItems: [URLQueryItem(name: "token", value: token), URLQueryItem(name: "device", value: deviceId)]) return url @@ -115,9 +151,9 @@ final class AppConfig: @unchecked Sendable { /// The chat2 log-relay room (docs/chat2-sync.md B) — replaces the s2 /// session rooms, which mobile no longer dials at all. `device` rides the /// URL so the DO can attribute sockets and honor excludeOwn backfills. - func chat2SocketURL(chatId: String) async -> URL? { - guard let token = await currentToken() else { return nil } - var url = wsBase.appending(path: "chat2/\(chatId)/ws") + func chat2SocketURL(chatId: String, encrypted: Bool = false) async -> URL? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var url = wsBase.appending(path: "chat2/\(encrypted ? MobileVault.encryptedRoomId(chatId) : chatId)/ws") url.append(queryItems: [URLQueryItem(name: "token", value: token), URLQueryItem(name: "device", value: deviceId)]) return url @@ -125,9 +161,9 @@ final class AppConfig: @unchecked Sendable { /// GET /chat2/{chatId}/checkpoint — the Range-resumable doc snapshot /// (auth via bearer header; the caller adds Range on resume). - func chat2CheckpointRequest(chatId: String) async -> URLRequest? { - guard let token = await currentToken() else { return nil } - var request = URLRequest(url: edgeURL.appending(path: "chat2/\(chatId)/checkpoint")) + func chat2CheckpointRequest(chatId: String, encrypted: Bool = false) async -> URLRequest? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var request = URLRequest(url: edgeURL.appending(path: "chat2/\(encrypted ? MobileVault.encryptedRoomId(chatId) : chatId)/checkpoint")) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") return request } @@ -135,9 +171,9 @@ final class AppConfig: @unchecked Sendable { /// GET /chat2/{chatId}/rows?after= — pull over plain HTTPS: one request /// collapses the socket's connect→hello→state→rowsReq→backfill, and it /// works on networks that strip WS upgrades (airplane wifi). - func chat2RowsRequest(chatId: String, after: UInt64) async -> URLRequest? { - guard let token = await currentToken() else { return nil } - var url = edgeURL.appending(path: "chat2/\(chatId)/rows") + func chat2RowsRequest(chatId: String, after: UInt64, encrypted: Bool = false) async -> URLRequest? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var url = edgeURL.appending(path: "chat2/\(encrypted ? MobileVault.encryptedRoomId(chatId) : chatId)/rows") url.append(queryItems: [URLQueryItem(name: "after", value: String(after)), URLQueryItem(name: "device", value: deviceId)]) var request = URLRequest(url: url) @@ -147,9 +183,9 @@ final class AppConfig: @unchecked Sendable { /// POST /chat2/{chatId}/rows?batchId= — push over plain HTTPS (batchId /// dedupe makes replays no-ops); body is the raw update batch. - func chat2PushRequest(chatId: String, batchId: String) async -> URLRequest? { - guard let token = await currentToken() else { return nil } - var url = edgeURL.appending(path: "chat2/\(chatId)/rows") + func chat2PushRequest(chatId: String, batchId: String, encrypted: Bool = false) async -> URLRequest? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var url = edgeURL.appending(path: "chat2/\(encrypted ? MobileVault.encryptedRoomId(chatId) : chatId)/rows") url.append(queryItems: [URLQueryItem(name: "batchId", value: batchId), URLQueryItem(name: "device", value: deviceId)]) var request = URLRequest(url: url) @@ -160,9 +196,9 @@ final class AppConfig: @unchecked Sendable { /// GET /registry/{orgId}/rows?since= — the WS hello's delta answer over /// plain HTTPS. `beat=1` doubles as a presence beat. - func registryRowsRequest(since: UInt64?) async -> URLRequest? { - guard let token = await currentToken() else { return nil } - var url = edgeURL.appending(path: "registry/\(orgId)/rows") + func registryRowsRequest(since: UInt64?, encrypted: Bool = false) async -> URLRequest? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var url = edgeURL.appending(path: "registry/\(orgId)\(encrypted ? "/e1" : "")/rows") var items = [URLQueryItem(name: "device", value: deviceId), URLQueryItem(name: "beat", value: "1")] if let since { items.append(URLQueryItem(name: "since", value: String(since))) } @@ -176,9 +212,9 @@ final class AppConfig: @unchecked Sendable { /// POST /registry/{orgId}/push — one op batch over plain HTTPS (LWW /// clocks make replays apply zero ops). - func registryPushRequest() async -> URLRequest? { - guard let token = await currentToken() else { return nil } - var url = edgeURL.appending(path: "registry/\(orgId)/push") + func registryPushRequest(encrypted: Bool = false) async -> URLRequest? { + guard let token = await syncToken(encrypted: encrypted) else { return nil } + var url = edgeURL.appending(path: "registry/\(orgId)\(encrypted ? "/e1" : "")/push") url.append(queryItems: [URLQueryItem(name: "device", value: deviceId)]) var request = URLRequest(url: url) request.httpMethod = "POST" diff --git a/apps/ios/Zeron/App/AppModel.swift b/apps/ios/Zeron/App/AppModel.swift index 1c005b4b5..efa2ba7bf 100644 --- a/apps/ios/Zeron/App/AppModel.swift +++ b/apps/ios/Zeron/App/AppModel.swift @@ -23,6 +23,15 @@ final class AppModel { /// Graced connectivity truth — one stream every consumer inherits calm /// from (home pill, composer notice, Queued/Failed badges). let connectivity = ConnectivityCenter() + /// Encrypted-sync vault state for the active profile (RFC 0001 §4.3). + /// Stores are built only after the vault decides whether this profile + /// syncs in the clear (no vault anywhere), encrypted (approved), or not + /// at all (a vault exists but this device is not yet approved). + private(set) var vaultStatus: MobileVaultStatus? + private(set) var vaultBusy = false + /// Keep the reapproval flow visible through pending/key-delivery states. + private(set) var requiresVaultApproval = false + @ObservationIgnored private var vaultTask: Task? private var sessionStores: [String: SessionStore] = [:] private var config: AppConfig? @ObservationIgnored private var pathMonitor: NWPathMonitor? @@ -230,11 +239,16 @@ final class AppModel { } func signOut() { + vaultTask?.cancel() + vaultTask = nil + config?.invalidate() workspace?.stop() workspace = nil sessionStores.values.forEach { $0.stop() } sessionStores.removeAll() config = nil + vaultStatus = nil + requiresVaultApproval = false demo = nil Keychain.delete(key: "accessToken") Keychain.delete(key: "refreshToken") @@ -254,11 +268,89 @@ final class AppModel { deviceId: deviceId, deviceName: deviceName, tokens: tokens, devBearer: devBearer) self.config = config - let store = WorkspaceStore(config: config) - workspace = store - store.start() + vaultStatus = nil startConnectivity() phase = .ready + // The vault decides the transport mode BEFORE any store dials: a + // profile with a vault never joins a plaintext room from this app. + refreshVault() + } + + // MARK: Encrypted sync (vault) + + /// Map the vault phase to what the stores may do. Anything that is not an + /// explicit "no vault" answer or an approved, key-holding membership + /// blocks sync entirely — never plaintext by default. + private static func syncAccess(for phase: MobileVaultPhase) -> AppConfig.SyncAccess { + switch phase { + case .legacy: return .legacy + case .ready: return .encrypted + default: return .blocked + } + } + + /// Re-run the vault reconcile and (re)build the stores when the sync + /// mode changes. Safe to call repeatedly (foreground, pairing polls). + func refreshVault() { + guard let config, demo == nil, vaultTask == nil else { return } + vaultBusy = true + vaultTask = Task { [weak self] in + let status = await config.vault.refresh(client: config.vaultClient) + await MainActor.run { [weak self] in + self?.vaultTask = nil + self?.vaultBusy = false + self?.applyVault(status, config: config) + } + } + } + + private func applyVault(_ status: MobileVaultStatus, config: AppConfig) { + guard self.config === config else { return } + vaultStatus = status + if status.phase == .revoked || status.phase == .pending || status.phase == .notEnrolled { requiresVaultApproval = true } + if status.phase == .ready || status.phase == .legacy { requiresVaultApproval = false } + let access = Self.syncAccess(for: status.phase) + let previous = config.syncAccess + config.setSyncAccess(access) + if access == .blocked || access != previous || workspace == nil { + // Mode changed (or first decision): every store was built for the + // old mode's rooms and codec. Rebuild from disk under the new one. + workspace?.stop() + workspace = nil + sessionStores.values.forEach { $0.stop() } + sessionStores.removeAll() + if access != .blocked { + let store = WorkspaceStore(config: config) + workspace = store + store.start() + } + } + if status.phase == .ready, let id = status.deviceId { + workspace?.publishDeviceIdentity(vaultId: id) + } + } + + /// Ask an approved device to admit this phone. `fingerprint` is the vault + /// fingerprint shown on that device (Settings → Encryption / `zeron vault + /// status`); it pins the genesis so a relay cannot substitute a vault. + func enrollVault(fingerprintHex: String) async throws { + guard let config else { throw MobileVaultError.unavailable } + guard let fingerprint = Data(vaultHex: fingerprintHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), count: 32) else { + throw MobileVaultError.verification + } + try await config.vault.enroll(fingerprint: fingerprint, client: config.vaultClient) + refreshVault() + } + + /// Rejoin with the recovery kit and the vault fingerprint from the + /// recovery file. + func recoverVault(kit: String, fingerprintHex: String) async throws { + guard let config else { throw MobileVaultError.unavailable } + guard let fingerprint = Data(vaultHex: fingerprintHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), count: 32) else { + throw MobileVaultError.verification + } + try await config.vault.recover(kit: kit, fingerprint: fingerprint, client: config.vaultClient) + refreshVault() } /// Wire the graced-connectivity recompute over the live stores (the @@ -540,6 +632,8 @@ final class AppModel { func foregrounded() { kickAllRooms() probeEdgeHealth() + // Previously approved devices can be revoked while suspended, too. + refreshVault() } private func probeEdgeHealth() { diff --git a/apps/ios/Zeron/App/ZeronApp.swift b/apps/ios/Zeron/App/ZeronApp.swift index b8a2d8438..3edf43f2b 100644 --- a/apps/ios/Zeron/App/ZeronApp.swift +++ b/apps/ios/Zeron/App/ZeronApp.swift @@ -37,6 +37,7 @@ struct ZeronApp: App { struct RootView: View { @Environment(AppModel.self) private var model + @Environment(\.scenePhase) private var scenePhase var body: some View { Group { @@ -46,9 +47,22 @@ struct RootView: View { case .pickingOrg(let tokens, let orgs): OrgPickerView(tokens: tokens, orgs: orgs) case .ready: - HomeView() + if model.requiresVaultApproval { + DeviceApprovalView() + } else { + HomeView() + } } } .task { model.restore() } + .task(id: scenePhase) { + guard scenePhase == .active else { return } + // Membership changes are independent of socket connectivity. + // Stop polling in the background; foregrounding checks immediately. + while !Task.isCancelled { + model.refreshVault() + do { try await Task.sleep(for: .seconds(5)) } catch { return } + } + } } } diff --git a/apps/ios/Zeron/Info.plist b/apps/ios/Zeron/Info.plist index 67a586a69..e8f7833ab 100644 --- a/apps/ios/Zeron/Info.plist +++ b/apps/ios/Zeron/Info.plist @@ -65,6 +65,8 @@ UIUserInterfaceStyle Dark + NSLocalNetworkUsageDescription + Connect to your desktop and local sync server to test encrypted sync. NSAppTransportSecurity NSAllowsLocalNetworking diff --git a/apps/ios/Zeron/Sync/ChatRoomClient.swift b/apps/ios/Zeron/Sync/ChatRoomClient.swift index 9ba1ae2f1..c108ff3b0 100644 --- a/apps/ios/Zeron/Sync/ChatRoomClient.swift +++ b/apps/ios/Zeron/Sync/ChatRoomClient.swift @@ -22,7 +22,10 @@ import os /// down). Visible in Console.app / `log stream` under this subsystem. let roomLog = Logger(subsystem: "sh.zeron.ios", category: "sync") +enum ChatApplyOutcome: Sendable { case applied, unavailable, authenticationFailed, storageFailed, pendingDependencies } + enum ChatRoomEvent: Sendable { + case paused /// Joined (or re-joined) and the initial catch-up (checkpoint if needed + /// row backfill) has been delivered through the apply closures. case connected @@ -59,9 +62,10 @@ actor ChatRoomClient { /// failure (the session redials rather than run blind). struct Delegate: Sendable { var cursor: @MainActor @Sendable () -> UInt64 - var containsFrontier: @MainActor @Sendable (Data) -> Bool - var applyCheckpoint: @MainActor @Sendable (Data, UInt64) -> Bool - var applyRow: @MainActor @Sendable (Data, UInt64) -> Void + var containsFrontier: @MainActor @Sendable (Data) async -> Bool + var applyCheckpoint: @MainActor @Sendable (Data, UInt64) async -> ChatApplyOutcome + var applyRow: @MainActor @Sendable (Data, UInt64) async -> ChatApplyOutcome + var acknowledged: @MainActor @Sendable (String) -> Void = { _ in } var advanceCursor: @MainActor @Sendable (UInt64) -> Void /// Cursor amnesty: lower the cursor to the checkpoint seq (no-op if /// already at or below). See the once-per-session clamp in @@ -92,6 +96,10 @@ actor ChatRoomClient { /// POST /chat2/{id}/rows?batchId= — the HTTPS push twin. private let pushRequest: @Sendable (String) async -> URLRequest? private let delegate: Delegate + private let sendAllowed: @Sendable () -> Bool + private let session = URLSession(configuration: .default) + private var paused: ChatApplyOutcome? + private var needsCheckpoint = false private var socket: URLSessionWebSocketTask? private var pullTask: Task? @@ -155,7 +163,8 @@ actor ChatRoomClient { checkpointRequest: @escaping @Sendable () async -> URLRequest?, rowsRequest: @escaping @Sendable (UInt64) async -> URLRequest?, pushRequest: @escaping @Sendable (String) async -> URLRequest?, - delegate: Delegate) { + delegate: Delegate, + sendAllowed: @escaping @Sendable () -> Bool = { true }) { self.chatId = chatId self.device = device self.urlProvider = urlProvider @@ -163,6 +172,7 @@ actor ChatRoomClient { self.rowsRequest = rowsRequest self.pushRequest = pushRequest self.delegate = delegate + self.sendAllowed = sendAllowed } // MARK: Lifecycle @@ -189,7 +199,7 @@ actor ChatRoomClient { /// makes replays no-ops), then pull rows (GET) and apply them through the /// exact frame path the socket uses. The airplane-wifi transport. func pullSync() async { - guard !closed else { return } + guard !closed, sendAllowed() else { return } // Push first, so a message typed on dead wifi leaves the device on // this cycle rather than the next. for push in pending { @@ -198,7 +208,7 @@ actor ChatRoomClient { break } request.httpBody = push.bytes - guard let (data, response) = try? await URLSession.shared.data(for: request), + guard let (data, response) = try? await session.data(for: request), let http = response as? HTTPURLResponse else { roomLog.warning("chat2 \(self.chatId, privacy: .public): http push transport error; will retry") break @@ -207,11 +217,12 @@ actor ChatRoomClient { let ack = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let seq = (ack["seq"] as? NSNumber)?.uint64Value { pending.removeAll { $0.batchId == push.batchId } + await delegate.acknowledged(push.batchId) // Contiguity rule (see the socket ACK path): a jump would // stamp the cursor over interleaved rows we never pulled — // hold, and this cycle's pull below walks the gap. let cursor = await delegate.cursor() - if seq <= cursor + 1 { + if paused == nil && (seq <= cursor || seq - cursor == 1) { await delegate.advanceCursor(seq) } else { roomLog.warning("chat2 \(self.chatId, privacy: .public): http ack gap (seq=\(seq), cursor=\(cursor)); holding cursor") @@ -237,7 +248,7 @@ actor ChatRoomClient { roomLog.warning("chat2 \(self.chatId, privacy: .public): http pull skipped — no URL (token unavailable)") return } - let fetched = try? await URLSession.shared.data(for: request) + let fetched = try? await session.data(for: request) guard let (body, response) = fetched, let http = response as? HTTPURLResponse else { roomLog.warning("chat2 \(self.chatId, privacy: .public): http pull transport error; will retry") return @@ -271,7 +282,7 @@ actor ChatRoomClient { } let planAfter = await delegate.cursor() var contained = state.checkpointSize == 0 - if !contained { + if !contained && !needsCheckpoint { contained = await delegate.containsFrontier(stateFrame.payload) } if case .checkpointThenRows = chatPlanCatchUp(cursor: planAfter, state: state, @@ -304,6 +315,7 @@ actor ChatRoomClient { } func stop() { + session.invalidateAndCancel() closed = true generation += 1 cancelTasks() @@ -323,6 +335,31 @@ actor ChatRoomClient { /// local doc; on a viewer device there is no checkpoint fallback, so /// this is loud (post-strip updates are KB-scale — an over-cap update is /// an upstream bug). + func enqueueSealed(id: String, bytes: Data) { + guard !closed, bytes.count <= Self.maxPushBytes, !pending.contains(where: { $0.batchId == id }) else { return } + pending.append(PendingPush(batchId: id, bytes: bytes)) + if stateReceived { Task { await self.pushPending() } } + } + + func resume(force: Bool = false) { + guard !closed, paused != nil else { return } + if paused == .authenticationFailed && !force { return } + paused = nil + connect() + } + + private func failedApply(_ outcome: ChatApplyOutcome) async { + switch outcome { + case .applied: return + case .storageFailed, .pendingDependencies: + needsCheckpoint = outcome == .pendingDependencies + await onSocketError(gen: generation) + case .unavailable, .authenticationFailed: + paused = outcome + await delegate.event(.paused) + } + } + func enqueue(update: Data) { guard update.count <= ChatRoomClient.maxPushBytes else { roomLog.error("chat2 \(self.chatId, privacy: .public): update \(update.count)B exceeds the row cap; not queued") @@ -400,7 +437,7 @@ actor ChatRoomClient { private func openSocket(url: URL, gen: Int) async { guard gen == generation, !closed else { return } - let task = URLSession.shared.webSocketTask(with: url) + let task = session.webSocketTask(with: url) socket = task task.resume() lastInbound = .now() @@ -571,16 +608,18 @@ actor ChatRoomClient { case ChatFrameType.ack: guard let batchId = frame.header["batchId"] as? String, - let seq = (frame.header["seq"] as? NSNumber)?.uint64Value else { return } + let seq = (frame.header["seq"] as? NSNumber)?.uint64Value, + pending.contains(where: { $0.batchId == batchId }) else { return } pending.removeAll { $0.batchId == batchId } + await delegate.acknowledged(batchId) // Contiguity rule, ack flavor: our own batch landing at `seq` // proves rows up to seq exist SERVER-side, not that we have the // interleaved ones from other devices. let cursor = await delegate.cursor() - if seq > cursor + 1 { + if seq > cursor && seq - cursor > 1 { gapRepair = true roomLog.warning("chat2 \(self.chatId, privacy: .public): ack gap (seq=\(seq), cursor=\(cursor)); holding cursor and requesting backfill") - } else { + } else if paused == nil { await delegate.advanceCursor(seq) } // A grant after a quota rejection: drain whatever the error @@ -624,7 +663,7 @@ actor ChatRoomClient { // Same presence rule as chatPlanCatchUp: SIZE, not seq — a seeded // room's checkpoint covers seq 0. var contained = state.checkpointSize == 0 - if !contained { + if !contained && !needsCheckpoint { contained = await delegate.containsFrontier(frame.payload) } // Cursor amnesty, once per client: a cursor above the checkpoint seq @@ -670,7 +709,7 @@ actor ChatRoomClient { // amnesty already applied) or UP (a contained checkpoint covers the // skipped span). Without the raise, the backfill's first row // (`after + 1`) reads as a contiguity gap against a stale cursor. - await delegate.setCursor(after) + if case .rowsOnly = plan { await delegate.setCursor(after) } await send(ChatWire.encode(ChatFrameType.rowsReq, header: ["after": after, "excludeOwn": resumed])) // Pending pushes go before catch-up completes: batchId dedupe makes @@ -703,14 +742,17 @@ actor ChatRoomClient { } return } - guard await delegate.applyCheckpoint(bytes, seq), !closed else { + let outcome = await delegate.applyCheckpoint(bytes, seq) + guard outcome == .applied, !closed else { if !closed { roomLog.error("chat2 \(self.chatId, privacy: .public): checkpoint import failed; redialing") checkpointBuffer = nil - await onSocketError(gen: generation) + needsCheckpoint = true + await failedApply(outcome) } return } + needsCheckpoint = false while let frame = checkpointBuffer?.first { checkpointBuffer?.removeFirst() await applyRowFrame(frame) @@ -724,6 +766,7 @@ actor ChatRoomClient { /// Factored out so frames buffered during a parallel checkpoint fetch /// replay through exactly the live path. private func applyRowFrame(_ frame: ChatWireFrame) async { + guard !closed, paused == nil else { return } if frame.kind == ChatFrameType.row { guard let seq = (frame.header["seq"] as? NSNumber)?.uint64Value else { return } // Own-device rows can still arrive (first-backfill redownload, a @@ -738,12 +781,12 @@ actor ChatRoomClient { // repair. Skipping the hole was the random new-session // forever-hang: an empty doc under an advanced cursor. let cursor = await delegate.cursor() - if seq > cursor + 1 { + if seq > cursor && seq - cursor > 1 { gapRepair = true roomLog.warning("chat2 \(self.chatId, privacy: .public): row gap (seq=\(seq), cursor=\(cursor)); holding cursor and requesting backfill") - await delegate.applyRow(frame.payload, cursor) + await failedApply(delegate.applyRow(frame.payload, cursor)) } else { - await delegate.applyRow(frame.payload, seq) + await failedApply(delegate.applyRow(frame.payload, seq)) } return } @@ -768,6 +811,8 @@ actor ChatRoomClient { /// catch-up is the stronger repair. The HTTP pull path needs no send — /// its next cycle re-requests from the held cursor. private func maybeRepairGap(gen: Int) async { + guard paused == nil else { return } + if needsCheckpoint { await onSocketError(gen: gen); return } guard gapRepair, gen == generation, socket != nil, stateReceived else { return } gapRepair = false gapRepairs += 1 @@ -820,8 +865,10 @@ actor ChatRoomClient { // MARK: Outbound private func pushPending() async { - guard stateReceived else { return } - for ix in pending.indices where !pending[ix].inFlight { + guard stateReceived, !closed, sendAllowed() else { return } + let ids = pending.filter { !$0.inFlight }.map(\.batchId) + for id in ids { + guard let ix = pending.firstIndex(where: { $0.batchId == id }), !pending[ix].inFlight else { continue } pending[ix].inFlight = true let push = pending[ix] await send(ChatWire.encode(ChatFrameType.push, @@ -840,7 +887,7 @@ actor ChatRoomClient { } private func send(_ frame: Data) async { - guard let socket else { return } + guard !closed, sendAllowed(), let socket else { return } try? await socket.send(.data(frame)) } @@ -868,7 +915,7 @@ actor ChatRoomClient { if !got.isEmpty { request.setValue("bytes=\(got.count)-", forHTTPHeaderField: "Range") } - guard let (stream, response) = try? await URLSession.shared.bytes(for: request), + guard let (stream, response) = try? await session.bytes(for: request), let http = response as? HTTPURLResponse else { continue } let seq = http.value(forHTTPHeaderField: "x-chat2-checkpoint-seq") if let seq { @@ -890,6 +937,7 @@ actor ChatRoomClient { } do { for try await byte in stream { + guard got.count < 64 * 1024 * 1024 else { return nil } got.append(byte) // Download progress is liveness (see pingTick) — stamp // it every 8KB, not every byte. diff --git a/apps/ios/Zeron/Sync/DeviceRelayClient.swift b/apps/ios/Zeron/Sync/DeviceRelayClient.swift index 56ad08303..125012d4e 100644 --- a/apps/ios/Zeron/Sync/DeviceRelayClient.swift +++ b/apps/ios/Zeron/Sync/DeviceRelayClient.swift @@ -196,6 +196,9 @@ actor DeviceRelayClient { private var nextId: UInt64 = 1 private let pending = DeviceRpcPending() private var connected = false + private var channel: VaultChannel? + private var connectionTask: Task? + private var connectionGeneration: UInt64 = 0 /// Transport clock — any inbound (the DO's auto-pong included) counts. private var lastInbound = DispatchTime.now() /// Host-proof clock — echo replies and inbound RPC frames only. @@ -214,7 +217,25 @@ actor DeviceRelayClient { // MARK: Lifecycle private func connect() async throws { + guard config.syncAccess != .blocked else { throw RelayError.notConnected } if connected, socket != nil { return } + if let connectionTask { return try await connectionTask.value } + connectionGeneration &+= 1 + let generation = connectionGeneration + let task = Task { try await self.establish(generation: generation) } + connectionTask = task + do { + try await task.value + if connectionGeneration == generation { connectionTask = nil } + } catch { + if connectionGeneration == generation { teardown(error: .notConnected) } + throw error + } + } + + private func establish(generation: UInt64) async throws { + let encrypted = config.syncAccess == .encrypted + let credentials = encrypted ? try await config.vault.channelCredentials(client: config.vaultClient) : nil // Registry-dark dial parking: a device with positive stale-presence // evidence fails fast with zero dials (it was 3-dial bursts every // ~60s to a device offline for days). A live cached link above wins; @@ -237,9 +258,47 @@ actor DeviceRelayClient { URLQueryItem(name: "connId", value: UUID().uuidString.lowercased()), URLQueryItem(name: "token", value: token), ] + guard generation == connectionGeneration, config.permitsSync(encrypted: encrypted) else { + throw RelayError.notConnected + } let task = URLSession.shared.webSocketTask(with: components.url!) + task.maximumMessageSize = VaultChannel.maximum + 4096 socket = task task.resume() + if let credentials { + let handshake = try VaultChannelHandshake(deviceId: credentials.deviceId, staticKey: credentials.staticKey, + vaultId: credentials.membership.vaultId, generation: credentials.membership.generation) + // Cancel the socket to interrupt receive even when the relay never answers. + let deadline = Task { + try? await Task.sleep(nanoseconds: 10_000_000_000) + if !Task.isCancelled { task.cancel(with: .goingAway, reason: nil) } + } + defer { deadline.cancel() } + try await task.send(.data(Self.encodeFrame(header: #"{"s":"hs1","k":"chan"}"#, payload: handshake.first))) + while true { + let message = try await task.receive() + guard case .data(let bytes) = message else { continue } + guard let (header, payload) = Self.decodeFrame(bytes) else { throw MobileVaultError.verification } + if header.k == Self.relayKind { throw RelayError.hostOffline } + if header.k == Self.rpcKind { throw MobileVaultError.verification } + guard header.k == "chan" else { continue } + guard header.s == "hs2" else { throw MobileVaultError.verification } + let (third, secured) = try handshake.finish(payload) { id, key in + id != credentials.deviceId && credentials.membership.activeDevice(id)?.encryptionKey == key + } + guard await config.vault.acceptsChannel(peerId: secured.peerId, peerKey: secured.peerKey), + generation == connectionGeneration, config.permitsSync(encrypted: true) else { + throw MobileVaultError.verification + } + try await task.send(.data(Self.encodeFrame(header: #"{"s":"hs3","k":"chan"}"#, payload: third))) + guard generation == connectionGeneration else { throw RelayError.notConnected } + channel = secured + break + } + } + guard generation == connectionGeneration, config.permitsSync(encrypted: encrypted) else { + throw RelayError.notConnected + } connected = true lastInbound = .now() lastHostProof = .now() @@ -248,12 +307,11 @@ actor DeviceRelayClient { receiveTask = Task { [weak self] in while !Task.isCancelled { guard let self else { return } - guard let sock = await self.socket else { return } do { - let message = try await sock.receive() - await self.handleInbound(message) + let message = try await task.receive() + await self.handleInbound(message, generation: generation) } catch { - await self.teardown(error: .hostOffline) + await self.connectionFailed(generation: generation) return } } @@ -262,7 +320,7 @@ actor DeviceRelayClient { while !Task.isCancelled { try? await Task.sleep(nanoseconds: DeviceRelayClient.pingIntervalNs) guard let self else { return } - await self.keepaliveTick() + await self.keepaliveTick(generation: generation) } } // One echo immediately on connect: feature detection + instant proof @@ -275,6 +333,10 @@ actor DeviceRelayClient { } private func teardown(error: RelayError) { + connectionGeneration &+= 1 + connectionTask?.cancel() + connectionTask = nil + channel = nil receiveTask?.cancel() pingTask?.cancel() socket?.cancel(with: .goingAway, reason: nil) @@ -283,11 +345,28 @@ actor DeviceRelayClient { pending.failAll(error: error) } + private func connectionFailed(generation: UInt64) { + if generation == connectionGeneration { teardown(error: .hostOffline) } + } + + private func channelAllowed() async -> Bool { + if let channel { + guard config.permitsSync(encrypted: true) else { return false } + let accepted = await config.vault.acceptsChannel(peerId: channel.peerId, peerKey: channel.peerKey) + return accepted && config.permitsSync(encrypted: true) && self.channel === channel + } + return config.permitsSync(encrypted: false) + } + /// Keepalive + liveness in one 10s tick: judge the transport lease and /// the host-echo deadline, then ride a text ping (DO transport lease) and /// an echo frame (host proof) out together. - private func keepaliveTick() async { - guard socket != nil else { return } + private func keepaliveTick(generation: UInt64) async { + guard generation == connectionGeneration, socket != nil else { return } + guard await channelAllowed(), generation == connectionGeneration else { + connectionFailed(generation: generation) + return + } let now = DispatchTime.now().uptimeNanoseconds if now - lastInbound.uptimeNanoseconds > Self.silenceLeaseNs { roomLog.warning("relay \(self.deviceId, privacy: .public): socket silent past lease; dropping link") @@ -376,15 +455,23 @@ actor DeviceRelayClient { } private func send(_ data: Data, for id: UInt64) async { + guard pending.owns(id: id) else { return } + let generation = connectionGeneration guard let socket else { failRequest(id: id, error: .notConnected) return } do { - try await socket.send(.data(data)) + guard await channelAllowed(), self.socket === socket else { throw RelayError.notConnected } + let outgoing: Data + if let channel { + guard let (_, payload) = Self.decodeFrame(data) else { throw MobileVaultError.verification } + outgoing = Self.encodeFrame(header: #"{"s":"rpc","k":"chan"}"#, payload: try channel.seal(payload)) + } else { outgoing = data } + try await socket.send(.data(outgoing)) } catch { failRequest(id: id, error: .notConnected) - teardown(error: .notConnected) + connectionFailed(generation: generation) } } @@ -426,19 +513,31 @@ actor DeviceRelayClient { private func cancelStream(id: UInt64) async { guard pending.removeStreamForCancellation(id: id), let socket else { return } + let generation = connectionGeneration let frame: [String: Any] = ["id": id, "cancel": true] guard let payload = try? JSONSerialization.data(withJSONObject: frame) else { return } let data = Self.encodeFrame(header: #"{"s":"rpc","k":"rpc"}"#, payload: payload) do { - try await socket.send(.data(data)) + guard await channelAllowed(), self.socket === socket else { throw RelayError.notConnected } + let outgoing: Data + if let channel { + guard let (_, payload) = Self.decodeFrame(data) else { throw MobileVaultError.verification } + outgoing = Self.encodeFrame(header: #"{"s":"rpc","k":"chan"}"#, payload: try channel.seal(payload)) + } else { outgoing = data } + try await socket.send(.data(outgoing)) } catch { - teardown(error: .notConnected) + connectionFailed(generation: generation) } } // MARK: Inbound - private func handleInbound(_ message: URLSessionWebSocketTask.Message) { + private func handleInbound(_ message: URLSessionWebSocketTask.Message, generation: UInt64) async { + guard generation == connectionGeneration else { return } + guard await channelAllowed(), generation == connectionGeneration else { + connectionFailed(generation: generation) + return + } lastInbound = .now() switch message { case .string: @@ -446,7 +545,16 @@ actor DeviceRelayClient { case .data(let data): guard let (header, payload) = Self.decodeFrame(data) else { return } switch header.k { + case "chan": + guard let channel, header.s == "rpc" else { teardown(error: .notConnected); return } + do { + let opened = try channel.open(payload) + lastHostProof = .now() + echoSeen = true + handleRpcPayload(opened) + } catch { teardown(error: .notConnected) } case Self.rpcKind: + guard channel == nil else { teardown(error: .notConnected); return } // An inbound RPC frame comes from the host — proof enough. lastHostProof = .now() echoSeen = true diff --git a/apps/ios/Zeron/Sync/DocDisk.swift b/apps/ios/Zeron/Sync/DocDisk.swift index dc0fdb045..5d2fba4d9 100644 --- a/apps/ios/Zeron/Sync/DocDisk.swift +++ b/apps/ios/Zeron/Sync/DocDisk.swift @@ -78,12 +78,16 @@ enum DocDisk { /// Atomically persist the chat2 doc snapshot + its room cursor. static func saveChat2(doc: LoroDoc, id: String, cursor: UInt64) { - guard let snapshot = try? doc.export(mode: .snapshot) else { return } + try? persistChat2(doc: doc, id: id, cursor: cursor) + } + + static func persistChat2(doc: LoroDoc, id: String, cursor: UInt64) throws { + let snapshot = try doc.export(mode: .snapshot) var data = chat2Magic var le = cursor.littleEndian withUnsafeBytes(of: &le) { data.append(contentsOf: $0) } data.append(snapshot) - try? data.write(to: chat2URL(for: id), options: .atomic) + try VaultPersistence.writeDurably(data, to: chat2URL(for: id)) } /// LRU-prune session snapshots (the workspace registry blob is always diff --git a/apps/ios/Zeron/Sync/RegistryClient.swift b/apps/ios/Zeron/Sync/RegistryClient.swift index 2da9e8bfe..4f3c89ee3 100644 --- a/apps/ios/Zeron/Sync/RegistryClient.swift +++ b/apps/ios/Zeron/Sync/RegistryClient.swift @@ -53,13 +53,15 @@ actor RegistryClient { /// MainActor-isolated bridge to the store's RegistryDoc. struct Delegate: Sendable { var helloCursor: @MainActor @Sendable () -> UInt64? - var takePushable: @MainActor @Sendable () -> [RegistryPendingBatch] - var event: @MainActor @Sendable (RegistryEvent) -> Void + var takePushable: @MainActor @Sendable () async -> [RegistryPendingBatch] + var event: @MainActor @Sendable (RegistryEvent) async -> Void } private let device: String private let urlProvider: @Sendable () async -> URL? private let delegate: Delegate + private let session = URLSession(configuration: .default) + private let sendAllowed: @Sendable () -> Bool private var socket: URLSessionWebSocketTask? private var receiveTask: Task? @@ -81,10 +83,11 @@ actor RegistryClient { init(device: String, urlProvider: @escaping @Sendable () async -> URL?, - delegate: Delegate) { + delegate: Delegate, sendAllowed: @escaping @Sendable () -> Bool = { true }) { self.device = device self.urlProvider = urlProvider self.delegate = delegate + self.sendAllowed = sendAllowed } // MARK: Lifecycle @@ -95,6 +98,7 @@ actor RegistryClient { } func stop() { + session.invalidateAndCancel() closed = true generation += 1 cancelTasks() @@ -168,7 +172,7 @@ actor RegistryClient { private func openSocket(url: URL, gen: Int) async { guard gen == generation, !closed else { return } - let task = URLSession.shared.webSocketTask(with: url) + let task = session.webSocketTask(with: url) socket = task task.resume() lastInbound = .now() @@ -371,7 +375,7 @@ actor RegistryClient { } private func send(_ frame: some Encodable) async { - guard let socket, let data = try? JSONEncoder().encode(frame), + guard !closed, sendAllowed(), let socket, let data = try? JSONEncoder().encode(frame), let text = String(data: data, encoding: .utf8) else { return } try? await socket.send(.string(text)) } diff --git a/apps/ios/Zeron/Sync/RegistryCore.swift b/apps/ios/Zeron/Sync/RegistryCore.swift index 9b92f4d29..803fce470 100644 --- a/apps/ios/Zeron/Sync/RegistryCore.swift +++ b/apps/ios/Zeron/Sync/RegistryCore.swift @@ -155,6 +155,10 @@ struct RegistryRow: Hashable, Codable, Sendable { var fields: [String: JSONValue] /// Per-field last-write clocks. var clocks: [String: Hlc] + /// Lifecycle proof for the tombstone (RFC 0001 §9): the sealed record a + /// member authored for exactly this row and `delHlc`. Encrypted readers + /// accept a tombstone only with a verified proof; cleared on revival. + var delProof: JSONValue? = nil } enum RegistryOpType: String, Codable, Sendable { @@ -178,6 +182,9 @@ struct RegistryOp: Hashable, Codable, Sendable { /// Per-field clock overrides — re-seed pushes carry a row's ORIGINAL /// clocks so recovery never coarsens causality. var clocks: [String: Hlc]? + /// Lifecycle proof for a `delete` op (becomes the tombstone's + /// `delProof`). Only meaningful on deletes; required in encrypted rooms. + var proof: JSONValue? = nil } // MARK: - Validation (structural, mirrors validateOp in registry-core.ts) @@ -202,6 +209,9 @@ func validateOp(_ op: RegistryOp) -> String? { if !matches(kindRe, op.kind) { return "bad kind" } if !matches(idRe, op.id) { return "bad id" } if !matches(hlcRe, op.hlc) { return "bad hlc" } + if let proof = op.proof, op.op != .delete || proof.objectValue == nil { + return "bad proof" + } if op.op == .delete { if op.set != nil { return "delete carries set" } } else { @@ -247,13 +257,15 @@ func applyOp(_ row: RegistryRow?, _ op: RegistryOp) -> RegistryApplyResult { // Tombstone-on-missing guards against a late create racing the delete. return RegistryApplyResult( row: RegistryRow(kind: op.kind, id: op.id, seq: 0, deleted: true, - delHlc: op.hlc, fields: [:], clocks: [:]), + delHlc: op.hlc, fields: [:], clocks: [:], delProof: op.proof), changed: true) } let beats = gone.deleted ? hlcNewer(op.hlc, gone.delHlc) : hlcNewer(op.hlc, maxClock(gone)) guard beats else { return RegistryApplyResult(row: row, changed: false) } gone.deleted = true gone.delHlc = op.hlc + // The proof travels with the tombstone it authorizes. + gone.delProof = op.proof gone.fields = [:] gone.clocks = [:] return RegistryApplyResult(row: gone, changed: true) @@ -309,7 +321,7 @@ func rowToSeedOp(_ row: RegistryRow) -> RegistryOp { if row.deleted { return RegistryOp(kind: row.kind, id: row.id, op: .delete, set: nil, hlc: row.delHlc ?? encodeHlc(ms: 0, counter: 0, device: "seed"), - clocks: nil) + clocks: nil, proof: row.delProof) } return RegistryOp(kind: row.kind, id: row.id, op: .upsert, set: row.fields, hlc: maxClock(row) ?? encodeHlc(ms: 0, counter: 0, device: "seed"), diff --git a/apps/ios/Zeron/Sync/SessionSidecars.swift b/apps/ios/Zeron/Sync/SessionSidecars.swift new file mode 100644 index 000000000..bae70e716 --- /dev/null +++ b/apps/ios/Zeron/Sync/SessionSidecars.swift @@ -0,0 +1,71 @@ +import Foundation + +/// Sidecars are display-only. They never advance a room cursor or enter its CRDT. +struct SessionSidecars: Sendable { + static let maximum = 4 * 1024 * 1024 + let config: AppConfig + let chatId: String + let encrypted: Bool + var transport: @Sendable (URLRequest) async throws -> (Data, Int) = { request in + let (stream, response) = try await URLSession.shared.bytes(for: request) + guard let http = response as? HTTPURLResponse else { throw MobileVaultError.unavailable } + guard response.expectedContentLength <= maximum else { throw MobileVaultError.oversized } + var bytes = Data() + for try await byte in stream { + guard bytes.count < maximum else { throw MobileVaultError.oversized } + bytes.append(byte) + } + return (bytes, http.statusCode) + } + + func tail() async throws -> Data { + let room = encrypted ? MobileVault.encryptedRoomId(chatId) : chatId + return try await fetch(path: ["chat2", room, "tail"], purpose: .tail) + } + + static func blobPart(ref: String, chatId: String) throws -> String { + let parts = ref.split(separator: "/", omittingEmptySubsequences: false) + guard parts.count == 2, parts[0] == chatId, !parts[1].isEmpty, parts[1].utf8.count <= 200, + parts[1] != ".", parts[1] != "..", + parts[1].utf8.allSatisfy({ (48...57).contains($0) || (65...90).contains($0) + || (97...122).contains($0) || Array("._:#~-".utf8).contains($0) }) else { + throw MobileVaultError.verification + } + return String(parts[1]) + } + + func blob(ref: String) async throws -> String { + let part = try Self.blobPart(ref: ref, chatId: chatId) + let room = encrypted ? MobileVault.encryptedRoomId(chatId) : chatId + let data: Data + do { data = try await fetch(path: ["blob", room, part], purpose: .blob) } + catch MobileVaultError.http(404) where encrypted { + // Compatibility with earlier sealed blobs. fetch still requires + // a valid encrypted record; plaintext is never displayed. + data = try await fetch(path: ["blob", chatId, part], purpose: .blob) + } + guard let text = String(data: data, encoding: .utf8) else { throw MobileVaultError.verification } + return text + } + + private func fetch(path: [String], purpose: VaultContentPurpose) async throws -> Data { + guard config.permitsSync(encrypted: encrypted), let token = await config.currentToken() else { + throw MobileVaultError.unavailable + } + var url = config.edgeURL + for part in path { url.append(component: part) } + var request = URLRequest(url: url, timeoutInterval: 30) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + let (bytes, status) = try await transport(request) + guard status == 200 else { throw MobileVaultError.http(status) } + guard bytes.count <= Self.maximum else { throw MobileVaultError.oversized } + guard config.permitsSync(encrypted: encrypted) else { throw MobileVaultError.unavailable } + let opened: Data + if encrypted { + opened = try await config.vault.open(bytes, object: MobileVault.objectId(kind: "chat", id: chatId), + purpose: purpose, maximum: Self.maximum - 1024, client: config.vaultClient) + } else { opened = bytes } + guard config.permitsSync(encrypted: encrypted) else { throw MobileVaultError.unavailable } + return opened + } +} diff --git a/apps/ios/Zeron/Sync/SessionStore.swift b/apps/ios/Zeron/Sync/SessionStore.swift index e00b8b59e..a5dcbe5cf 100644 --- a/apps/ios/Zeron/Sync/SessionStore.swift +++ b/apps/ios/Zeron/Sync/SessionStore.swift @@ -72,6 +72,15 @@ final class SessionStore { private var chatRoom: ChatRoomClient? private var subscriptions: [Subscription] = [] private let config: AppConfig + let encrypted: Bool + private(set) var syncError: String? + @ObservationIgnored private var vaultState: VaultChatState + @ObservationIgnored private let pendingUpdates = VaultUpdateBuffer() + @ObservationIgnored private var sealing = false + @ObservationIgnored private var sealer: Task? + private var vaultDisk: VaultChatDisk { + VaultChatDisk(directory: config.vaultPersistence.directory, profile: config.vaultPersistence.account, chatId: chatId) + } /// Registry roomGen for this chat (M2): connect only at >= 2. One-way — /// the registry never walks a chat back to s2. @ObservationIgnored private var roomGen = 1 @@ -91,6 +100,8 @@ final class SessionStore { self.chatId = chatId self.config = config self.offline = offline + encrypted = config.syncAccess == .encrypted + vaultState = VaultChatState(profile: config.vaultPersistence.account, chatId: chatId) AttachmentImageCache.shared.configure(config: config) } @@ -152,7 +163,19 @@ final class SessionStore { // Local-first: the last-synced chat2 snapshot renders instantly (even // when the host device is offline); the join backfills incrementally // from its cursor. - if let saved = DocDisk.loadChat2(into: doc, id: chatId) { + if encrypted { + do { + if let saved = try vaultDisk.load() { + vaultState = saved + if !saved.snapshot.isEmpty { _ = try doc.importWith(bytes: saved.snapshot, origin: "disk") } + cursor = saved.cursor + project() + } + } catch { + syncError = "Encrypted chat storage could not be opened. Existing data was retained." + return + } + } else if let saved = DocDisk.loadChat2(into: doc, id: chatId) { cursor = saved project() } else if DocDisk.legacySnapshotExists(id: chatId) { @@ -163,17 +186,22 @@ final class SessionStore { } saver = DocSaver { [weak self] in guard let self else { return } - DocDisk.saveChat2(doc: self.doc, id: self.chatId, cursor: self.cursor) + do { try self.persist(cursor: self.cursor) } + catch { self.syncError = "Chat could not be saved; sync is paused." } } // Subscription BEFORE any connect: every local commit lands in the // client when it exists; commits made earlier are covered by the // first-contact full-log push below (cursor 0 whenever no client has // ever acked — see connectIfReady). - let localSub = doc.subscribeLocalUpdate { [weak self] update in + let localSub = doc.subscribeLocalUpdate { [weak self, pendingUpdates, encrypted] update in let bytes = Data(update) + if encrypted { pendingUpdates.append(bytes) } Task { @MainActor [weak self] in - guard let self else { return } - if let room = self.chatRoom { + guard let self, self.started else { return } + if self.encrypted { + do { try self.persist(cursor: self.cursor); self.startSealer() } + catch { self.syncError = "Chat could not be saved; nothing was sent." } + } else if let room = self.chatRoom { Task { await room.enqueue(update: bytes) } } self.saver?.poke() @@ -208,11 +236,131 @@ final class SessionStore { connectIfReady() } + private func persist(cursor nextCursor: UInt64) throws { + if encrypted { + vaultState.unsealed.append(contentsOf: pendingUpdates.take()) + var candidate = vaultState + candidate.snapshot = try doc.export(mode: .snapshot) + candidate.cursor = nextCursor + try vaultDisk.save(candidate) + vaultState = candidate + } else { + try DocDisk.persistChat2(doc: doc, id: chatId, cursor: nextCursor) + } + cursor = nextCursor + } + + private func applyRemote(_ bytes: Data, seq: UInt64, purpose: VaultContentPurpose) async -> ChatApplyOutcome { + guard started, config.permitsSync(encrypted: encrypted) else { return .unavailable } + let plaintext: Data + if encrypted { + do { + plaintext = try await config.vault.open(bytes, object: MobileVault.objectId(kind: "chat", id: chatId), + purpose: purpose, maximum: VaultContentCrypto.maxPlaintextBytes, + client: config.vaultClient) + } catch is VaultRecordError { return .authenticationFailed } + catch is VaultContentError { return .authenticationFailed } + catch is VaultPolicyError { return .authenticationFailed } + catch { return .unavailable } + } else { plaintext = bytes } + guard started, config.permitsSync(encrypted: encrypted) else { return .unavailable } + do { + let status = try doc.importWith(bytes: plaintext, origin: "remote") + if status.pending != nil { return .pendingDependencies } + try persist(cursor: max(cursor, seq)) + showingTail = false + project() + syncError = nil + return .applied + } catch { + syncError = "Chat could not be imported or saved; its cursor was held." + return .storageFailed + } + } + + private func encryptedFrontier(_ bytes: Data) async -> Bool { + guard let plaintext = try? await config.vault.open(bytes, object: MobileVault.objectId(kind: "chat", id: chatId), + purpose: .frontier, maximum: 64 * 1024, client: config.vaultClient), + let vv = try? VersionVector.decode(bytes: plaintext), !vv.toHashmap().isEmpty else { return false } + return doc.oplogVv().includesVv(other: vv) + } + + private func acknowledge(_ id: String) { + vaultState.unsealed.append(contentsOf: pendingUpdates.take()) + let previous = vaultState + vaultState.outbox.removeAll { $0.id == id } + do { try persist(cursor: cursor) } + catch { vaultState = previous; syncError = "Acknowledgement could not be saved; the batch will be retried." } + } + + private func startSealer() { + guard encrypted, !sealing, started, config.permitsSync(encrypted: true) else { return } + sealing = true + sealer = Task { [weak self] in + guard let self else { return } + defer { self.sealing = false } + let object = MobileVault.objectId(kind: "chat", id: self.chatId) + do { + try self.persist(cursor: self.cursor) + for batch in self.vaultState.outbox { + try Task.checkCancellation() + let (bytes, id) = try await self.config.vault.prepareBatch(batch.bytes, object: object, + maximum: ChatRoomClient.maxPushBytes - 512, client: self.config.vaultClient) + guard self.config.permitsSync(encrypted: true) else { return } + guard let index = self.vaultState.outbox.firstIndex(where: { $0.id == batch.id }) else { continue } + if id != batch.id { + self.vaultState.unsealed.append(contentsOf: self.pendingUpdates.take()) + let previous = self.vaultState + self.vaultState.outbox[index] = VaultChatBatch(id: id, bytes: bytes) + do { try self.persist(cursor: self.cursor) } + catch { self.vaultState = previous; throw error } + } + await self.chatRoom?.enqueueSealed(id: id, bytes: bytes) + } + while let update = self.vaultState.unsealed.first { + try Task.checkCancellation() + let (bytes, id) = try await self.config.vault.seal(update, object: object, purpose: .chatUpdate, + maximum: ChatRoomClient.maxPushBytes - 512, client: self.config.vaultClient) + guard self.config.permitsSync(encrypted: true) else { return } + self.vaultState.unsealed.append(contentsOf: self.pendingUpdates.take()) + let previous = self.vaultState + self.vaultState.unsealed.removeFirst() + self.vaultState.outbox.append(VaultChatBatch(id: id, bytes: bytes)) + do { try self.persist(cursor: self.cursor) } + catch { self.vaultState = previous; throw error } + await self.chatRoom?.enqueueSealed(id: id, bytes: bytes) + } + self.syncError = nil + } catch { + self.syncError = "Encrypted sends remain queued on this device. Retry when the vault is ready." + } + } + } + + func pauseNetwork() async { + sealer?.cancel() + let task = sealer + sealer = nil + let room = chatRoom + chatRoom = nil + await room?.stop() + await task?.value + connected = false + } + + func resumeNetwork(force: Bool = false) { + connectIfReady() + if let room = chatRoom { Task { await room.resume(force: force) } } + startSealer() + } + private func connectIfReady() { - guard started, !offline, !holdDial, chatRoom == nil, roomGen >= 2 else { return } + guard started, !offline, !holdDial, chatRoom == nil, roomGen >= 2, config.permitsSync(encrypted: encrypted) else { return } + loadTail() let delegate = ChatRoomClient.Delegate( cursor: { [weak self] in self?.cursor ?? 0 }, containsFrontier: { [weak self] frontier in + if let self, self.encrypted { return await self.encryptedFrontier(frontier) } // Deliberately NO empty-frontier shortcut (mirror of // EngineChatSink::contains_frontier): an empty payload on a // present checkpoint is unreadable provenance, not proof of @@ -232,31 +380,21 @@ final class SessionStore { return self.doc.oplogVv().includesVv(other: vv) }, applyCheckpoint: { [weak self] bytes, seq in - guard let self, - (try? self.doc.importWith(bytes: bytes, origin: "remote")) != nil else { - return false - } - self.cursor = max(self.cursor, seq) - self.project() - self.saver?.poke() - return true + guard let self else { return .storageFailed } + return await self.applyRemote(bytes, seq: seq, purpose: .checkpoint) }, applyRow: { [weak self] bytes, seq in - guard let self else { return } + guard let self else { return .storageFailed } // Malformed remote bytes cost the row, never the doc. The // cursor still advances: replaying a poison row forever is // the wedge class chat2 replaces. - if (try? self.doc.importWith(bytes: bytes, origin: "remote")) == nil { - roomLog.warning("chat2 \(self.chatId, privacy: .public): row import failed; skipping row \(seq)") - } - self.cursor = max(self.cursor, seq) - self.project() - self.saver?.poke() + return await self.applyRemote(bytes, seq: seq, purpose: .chatUpdate) }, + acknowledged: { [weak self] id in if let self, self.encrypted { self.acknowledge(id) } }, advanceCursor: { [weak self] seq in guard let self else { return } - self.cursor = max(self.cursor, seq) - self.saver?.poke() + do { try self.persist(cursor: max(self.cursor, seq)) } + catch { self.syncError = "Cursor could not be saved." } }, clampCursor: { [weak self] seq in guard let self, self.cursor > seq else { return } @@ -269,29 +407,30 @@ final class SessionStore { // (KB-bounded by trim policy; re-imports are no-ops), which // converts any lying cursor into a true one. roomLog.info("chat2 \(self.chatId, privacy: .public): cursor amnesty \(self.cursor) → \(seq)") - self.cursor = seq - self.saver?.poke() + do { try self.persist(cursor: seq) } + catch { self.syncError = "Cursor could not be saved." } }, setCursor: { [weak self] seq in guard let self, self.cursor != seq else { return } - self.cursor = seq - self.saver?.poke() + do { try self.persist(cursor: seq) } + catch { self.syncError = "Cursor could not be saved." } }, event: { [weak self] event in self?.handle(event) } ) let client = ChatRoomClient( chatId: chatId, device: config.deviceId, - urlProvider: { [config, chatId] in await config.chat2SocketURL(chatId: chatId) }, - checkpointRequest: { [config, chatId] in - await config.chat2CheckpointRequest(chatId: chatId) + urlProvider: { [config, chatId, encrypted] in await config.chat2SocketURL(chatId: chatId, encrypted: encrypted) }, + checkpointRequest: { [config, chatId, encrypted] in + await config.chat2CheckpointRequest(chatId: chatId, encrypted: encrypted) }, - rowsRequest: { [config, chatId] after in - await config.chat2RowsRequest(chatId: chatId, after: after) + rowsRequest: { [config, chatId, encrypted] after in + await config.chat2RowsRequest(chatId: chatId, after: after, encrypted: encrypted) }, - pushRequest: { [config, chatId] batchId in - await config.chat2PushRequest(chatId: chatId, batchId: batchId) + pushRequest: { [config, chatId, encrypted] batchId in + await config.chat2PushRequest(chatId: chatId, batchId: batchId, encrypted: encrypted) }, - delegate: delegate) + delegate: delegate, + sendAllowed: { [config, encrypted] in config.permitsSync(encrypted: encrypted) }) chatRoom = client // First contact with the room (cursor 0): everything committed // BEFORE the local-update subscription saw a client — an adopt's @@ -301,11 +440,12 @@ final class SessionStore { // on unpushed deps sit in peers' pending-dep buffers forever). Push // the doc's full update log as the join's first batch; once acked // the cursor moves and this never re-arms. - if cursor == 0, + if !encrypted, cursor == 0, let all = try? doc.export(mode: .updates(from: VersionVector())), !all.isEmpty { Task { await client.enqueue(update: all) } } Task { await client.start() } + startSealer() } /// Mine the retired s2 snapshot for OUR OWN still-pending commands and @@ -361,6 +501,12 @@ final class SessionStore { } func stop() { + started = false + sealer?.cancel() + tailTask?.cancel() + tailTask = nil + if let hostRelay { Task { await hostRelay.client.close() } } + hostRelay = nil subscriptions.removeAll() saver?.flush() if let chatRoom { @@ -372,6 +518,9 @@ final class SessionStore { private func handle(_ event: ChatRoomEvent) { switch event { + case .paused: + connected = false + syncError = "Sync paused: the next record could not be verified or opened." case .connected: connected = true project() @@ -380,6 +529,42 @@ final class SessionStore { } } + @ObservationIgnored private var tailTask: Task? + private var showingTail = false + + private func loadTail() { + guard tailTask == nil, entries.isEmpty, started, roomGen >= 2, !holdDial else { return } + let sidecars = SessionSidecars(config: config, chatId: chatId, encrypted: encrypted) + tailTask = Task { [weak self] in + defer { self?.tailTask = nil } + do { + let bytes = try await sidecars.tail() + guard let self, self.started, !Task.isCancelled, self.entries.isEmpty, + self.config.permitsSync(encrypted: self.encrypted) else { return } + let entries = try Self.decodeTail(bytes, chatId: self.chatId) + self.showingTail = true + self.apply(entries) + } catch { + // Missing/unavailable sidecars do not prevent authoritative room sync. + } + } + } + + func fetchToolBlob(ref: String) async throws -> String { + try await SessionSidecars(config: config, chatId: chatId, encrypted: encrypted).blob(ref: ref) + } + + nonisolated static func decodeTail(_ bytes: Data, chatId: String) throws -> [MessageEntry] { + guard let json = try JSONSerialization.jsonObject(with: bytes) as? [String: Any], + json["chatId"] as? String == chatId, json["schemaVersion"] as? Int == 1, + let messages = json["messages"] as? [[String: Any]], messages.count <= 64 else { + throw MobileVaultError.verification + } + let entries = messages.compactMap { entryFrom(.fromJSON($0)) } + guard entries.count == messages.count else { throw MobileVaultError.verification } + return joinContinuations(entries) + } + // MARK: Projection /// In-flight guard + trailing re-run for the off-main projection below. @@ -411,7 +596,7 @@ final class SessionStore { }.value guard let self else { return } self.projecting = false - if let decoded { + if let decoded, !self.showingTail { self.apply(decoded.entries, queue: decoded.queue) } if self.projectPending { @@ -477,10 +662,13 @@ final class SessionStore { fields[k] = list.map { "\($0.jsonObject)" } } } + for key in ["output", "outputRef", "diffRef"] { + if let value = m[key]?.stringValue { fields[key] = value } + } // isError presence IS the resolution marker (schema.rs:96). let isError = m["isError"]?.boolValue return .tool(id: id, call: RenderToolCall(tag: tag, fields: fields), - isError: isError ?? false, resolved: isError != nil) + isError: isError ?? false, resolved: m["resolved"]?.boolValue ?? (isError != nil)) case "input": var questions: [UserInputQuestion] = [] if let list = m["questions"]?.listValue, @@ -488,7 +676,7 @@ final class SessionStore { let decoded = try? JSONDecoder().decode([UserInputQuestion].self, from: data) { questions = decoded } - return .input(id: id, requestId: id, questions: questions, + return .input(id: id, requestId: m["requestId"]?.stringValue ?? id, questions: questions, resolved: m["resolved"]?.boolValue ?? false) case "error": return .error(id: id, message: m["message"]?.stringValue ?? "") @@ -639,7 +827,11 @@ final class SessionStore { try map.insert(key: "expiresAt", v: nowMs() + commandDefaultTtlMs) try map.insert(key: "status", v: "pending") doc.commit() - } catch {} + try persist(cursor: cursor) + } catch { + syncError = "Command could not be saved; nothing was sent." + return + } nudgeHost() } diff --git a/apps/ios/Zeron/Sync/VaultChannel.swift b/apps/ios/Zeron/Sync/VaultChannel.swift new file mode 100644 index 000000000..2fc440ea4 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultChannel.swift @@ -0,0 +1,160 @@ +import CryptoKit +import Foundation + +/// Fixed Noise_XX_25519_AESGCM_SHA256 initiator, matching crypto/channel.rs. +/// No negotiation or plaintext fallback. All primitives are supplied by CryptoKit. +final class VaultChannelHandshake { + private let identity: Curve25519.KeyAgreement.PrivateKey + private let ephemeral: Curve25519.KeyAgreement.PrivateKey + private let deviceId: Data + private var hash: Data + private var chainingKey: Data + private var cipher: VaultChannelCipher? + private var consumed = false + let first: Data + + init(deviceId: Data, staticKey: Data, vaultId: Data, generation: Data, + ephemeralKey: Curve25519.KeyAgreement.PrivateKey = .init()) throws { + guard deviceId.count == 16, vaultId.count == 16, generation.count == 16 else { + throw MobileVaultError.verification + } + self.deviceId = deviceId + identity = try .init(rawRepresentation: staticKey) + ephemeral = ephemeralKey + let initial = Data(SHA256.hash(data: Data("Noise_XX_25519_AESGCM_SHA256".utf8))) + // Protocol names shorter than HASHLEN are zero-padded, not hashed. + let name = Data("Noise_XX_25519_AESGCM_SHA256".utf8) + hash = name.count <= 32 ? name + Data(repeating: 0, count: 32 - name.count) : initial + chainingKey = hash + hash = Data(SHA256.hash(data: hash + Data("zeron/device-channel/v1\0".utf8) + vaultId + generation)) + first = ephemeral.publicKey.rawRepresentation + hash = Data(SHA256.hash(data: hash + first)) + hash = Data(SHA256.hash(data: hash)) // empty first payload + } + + func finish(_ second: Data, accept: (Data, Data) -> Bool) throws -> (Data, VaultChannel) { + guard !consumed, second.count == 112 else { throw MobileVaultError.verification } + consumed = true + let remoteEphemeral = Data(second.prefix(32)) + mixHash(remoteEphemeral) + try mixKey(ephemeral, remoteEphemeral) + let remoteStatic = try decrypt(Data(second.dropFirst(32).prefix(48))) + try mixKey(ephemeral, remoteStatic) + let remoteId = try decrypt(Data(second.suffix(32))) + guard remoteId.count == 16, accept(remoteId, remoteStatic) else { throw MobileVaultError.verification } + var third = try encrypt(identity.publicKey.rawRepresentation) + try mixKey(identity, remoteEphemeral) + third.append(try encrypt(deviceId)) + let keys = Self.derive(chainingKey, Data()) + return (third, VaultChannel(sendKey: keys.0, receiveKey: keys.1, + peerId: remoteId, peerKey: remoteStatic)) + } + + private func mixHash(_ bytes: Data) { hash = Data(SHA256.hash(data: hash + bytes)) } + private func mixKey(_ local: Curve25519.KeyAgreement.PrivateKey, _ remote: Data) throws { + let shared = try local.sharedSecretFromKeyAgreement(with: .init(rawRepresentation: remote)) + let keys = Self.derive(chainingKey, shared.withUnsafeBytes { Data($0) }) + chainingKey = keys.0 + cipher = VaultChannelCipher(key: keys.1) + } + private func decrypt(_ bytes: Data) throws -> Data { + guard let cipher else { throw MobileVaultError.verification } + let result = try cipher.open(bytes, aad: hash) + mixHash(bytes) + return result + } + private func encrypt(_ bytes: Data) throws -> Data { + guard let cipher else { throw MobileVaultError.verification } + let result = try cipher.seal(bytes, aad: hash) + mixHash(result) + return result + } + private static func derive(_ salt: Data, _ input: Data) -> (Data, Data) { + let temp = Data(HMAC.authenticationCode(for: input, using: SymmetricKey(data: salt))) + let first = Data(HMAC.authenticationCode(for: Data([1]), using: SymmetricKey(data: temp))) + let second = Data(HMAC.authenticationCode(for: first + Data([2]), using: SymmetricKey(data: temp))) + return (first, second) + } +} + +private final class VaultChannelCipher { + private let key: SymmetricKey + private var counter: UInt64 = 0 + init(key: Data) { self.key = SymmetricKey(data: key) } + private func nonce() throws -> AES.GCM.Nonce { + guard counter < 1 << 32 else { throw MobileVaultError.verification } + var value = counter.bigEndian + return try AES.GCM.Nonce(data: Data(repeating: 0, count: 4) + withUnsafeBytes(of: &value) { Data($0) }) + } + func seal(_ bytes: Data, aad: Data = Data()) throws -> Data { + let box = try AES.GCM.seal(bytes, using: key, nonce: nonce(), authenticating: aad) + counter += 1 + return box.ciphertext + box.tag + } + func open(_ bytes: Data, aad: Data = Data()) throws -> Data { + guard bytes.count >= 16 else { throw MobileVaultError.verification } + let box = try AES.GCM.SealedBox(nonce: nonce(), ciphertext: bytes.dropLast(16), tag: bytes.suffix(16)) + let result = try AES.GCM.open(box, using: key, authenticating: aad) + counter += 1 + return result + } +} + +/// Actor-owned ordered transport. Any failure permanently retires this channel. +final class VaultChannel { + static let maximum = 8 * 1024 * 1024 + private static let chunkSize = 65518 + let peerId: Data + let peerKey: Data + private let sender: VaultChannelCipher + private let receiver: VaultChannelCipher + private var failed = false + init(sendKey: Data, receiveKey: Data, peerId: Data, peerKey: Data) { + sender = VaultChannelCipher(key: sendKey) + receiver = VaultChannelCipher(key: receiveKey) + self.peerId = peerId + self.peerKey = peerKey + } + func seal(_ bytes: Data) throws -> Data { + guard !failed, bytes.count <= Self.maximum else { failed = true; throw MobileVaultError.oversized } + do { + var result = Data() + var offset = 0 + repeat { + let end = min(offset + Self.chunkSize, bytes.count) + let sealed = try sender.seal(Data([end < bytes.count ? 1 : 0]) + bytes.subdata(in: offset..> 8)) + result.append(UInt8(sealed.count & 255)) + result.append(sealed) + offset = end + } while offset < bytes.count + return result + } catch { failed = true; throw error } + } + func open(_ bytes: Data) throws -> Data { + guard !failed, bytes.count <= Self.maximum + (Self.maximum / Self.chunkSize + 1) * 19 else { + failed = true + throw MobileVaultError.oversized + } + do { + let bytes = [UInt8](bytes) + var offset = 0 + var result = Data() + while true { + guard offset + 2 <= bytes.count else { throw MobileVaultError.verification } + let count = Int(bytes[offset]) * 256 + Int(bytes[offset + 1]) + offset += 2 + guard count >= 17, offset + count <= bytes.count else { throw MobileVaultError.verification } + let plaintext = try receiver.open(Data(bytes[offset.. [Data] { + lock.withLock { + let result = updates + updates.removeAll() + return result + } + } +} + +struct VaultChatBatch: Codable, Equatable, Sendable { + var id: String + var bytes: Data +} + +struct VaultChatState: Codable, Sendable { + var version = 1 + var profile: String + var chatId: String + var snapshot = Data() + var cursor: UInt64 = 0 + var unsealed: [Data] = [] + var outbox: [VaultChatBatch] = [] +} + +struct VaultChatDisk: Sendable { + static let maxBytes = 64 * 1024 * 1024 + let directory: URL + let profile: String + let chatId: String + var url: URL { + directory.appendingPathComponent(Data(SHA256.hash(data: Data(chatId.utf8))).vaultHex + ".chat") + } + + func load() throws -> VaultChatState? { + guard FileManager.default.fileExists(atPath: url.path) else { return nil } + guard let size = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize, size <= Self.maxBytes else { throw VaultStorageError.tooLarge } + let state = try JSONDecoder().decode(VaultChatState.self, from: Data(contentsOf: url)) + guard state.version == 1, state.profile == profile, state.chatId == chatId, + Set(state.outbox.map(\.id)).count == state.outbox.count else { throw VaultStorageError.invalidState } + for batch in state.outbox { + let record = try VaultUnverifiedRecord.parse(batch.bytes, maxPayloadBytes: 1024 * 1024) + guard record.untrustedRevisionId.vaultHex == batch.id, record.untrustedBinding.kind == .content else { throw VaultStorageError.invalidState } + } + return state + } + + func save(_ state: VaultChatState) throws { + guard state.profile == profile, state.chatId == chatId else { throw VaultStorageError.invalidState } + let data = try JSONEncoder().encode(state) + guard data.count <= Self.maxBytes else { throw VaultStorageError.tooLarge } + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let files = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.fileSizeKey]) + var total = data.count + for file in files where file.pathExtension == "chat" && file != url { + total += try file.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 + guard total <= Self.maxBytes else { throw VaultStorageError.tooLarge } + } + try VaultPersistence.writeDurably(data, to: url) + } +} diff --git a/apps/ios/Zeron/Sync/VaultContent.swift b/apps/ios/Zeron/Sync/VaultContent.swift new file mode 100644 index 000000000..e180b148d --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultContent.swift @@ -0,0 +1,214 @@ +import CryptoKit +import Foundation +import Security + +enum VaultContentError: Error, Equatable { + case record(VaultRecordError) + case crypto(VaultCryptoError) + case invalidKey + case invalidSigningKey + case wrongScope + case wrongAuthor + case wrongKind + case wrongKey + case wrongPurpose + case unsupportedFormat + case unsupportedSuite + case unsupportedPurpose + case sizeLimitExceeded + case entropyUnavailable + case cryptographyFailed +} + +enum VaultContentPurpose: UInt64 { + case chatUpdate = 1, checkpoint = 2, frontier = 3, registryField = 4 + case tail = 5, diff = 6, blob = 7, deviceSidecar = 8, registryLifecycle = 9 +} + +struct VaultKeyScope: Equatable { + let vaultId: Data + let generation: Data + let epoch: UInt64 + let objectId: Data + + init(_ binding: VaultRecordBinding) { + vaultId = binding.vaultId + generation = binding.generation + epoch = binding.epoch + objectId = binding.objectId + } +} + +final class VaultContentKey: CustomStringConvertible, CustomDebugStringConvertible { + let scope: VaultKeyScope + let identifier: Data + fileprivate let material: SymmetricKey + + init(scope: VaultKeyScope, identifier: Data, bytes: Data) throws { + guard scope.epoch > 0, [scope.vaultId, scope.generation, scope.objectId].allSatisfy({ $0.count == 16 }) else { + throw VaultContentError.wrongScope + } + guard identifier.count == 16, bytes.count == 32 else { throw VaultContentError.invalidKey } + self.scope = scope + self.identifier = identifier + material = SymmetricKey(data: bytes) + } + + static func generate(scope: VaultKeyScope) throws -> VaultContentKey { + guard scope.epoch > 0, [scope.vaultId, scope.generation, scope.objectId].allSatisfy({ $0.count == 16 }) else { + throw VaultContentError.wrongScope + } + let identifier = try VaultContentCrypto.randomBytes(16) + var secret = try VaultContentCrypto.randomBytes(32) + defer { secret.resetBytes(in: secret.startIndex.. Data { material.withUnsafeBytes { Data($0) } } + var description: String { "ContentKey([REDACTED])" } + var debugDescription: String { description } +} + +final class VaultDeviceSigner: CustomStringConvertible, CustomDebugStringConvertible { + let authorId: Data + fileprivate let key: Curve25519.Signing.PrivateKey + + init(authorId: Data, seed: Data) throws { + guard authorId.count == 16, seed.count == 32 else { throw VaultContentError.invalidSigningKey } + self.authorId = authorId + do { key = try Curve25519.Signing.PrivateKey(rawRepresentation: seed) } + catch { throw VaultContentError.invalidSigningKey } + guard VaultCrypto.passesEd25519PointEncodingPrecheck(key.publicKey.rawRepresentation) else { + throw VaultContentError.invalidSigningKey + } + } + + var publicKey: Data { key.publicKey.rawRepresentation } + var description: String { "DeviceSigner([REDACTED])" } + var debugDescription: String { description } +} + +struct VaultSealedContent: CustomStringConvertible, CustomDebugStringConvertible { + let binding: VaultRecordBinding + let purpose: VaultContentPurpose + let revisionId: Data + let encoded: Data + fileprivate init(binding: VaultRecordBinding, purpose: VaultContentPurpose, revisionId: Data, encoded: Data) { + self.binding = binding + self.purpose = purpose + self.revisionId = revisionId + self.encoded = encoded + } + var description: String { "SealedContent([REDACTED])" } + var debugDescription: String { description } +} + +struct VaultOpenedContent: CustomStringConvertible, CustomDebugStringConvertible { + let revisionId: Data + let plaintext: Data + fileprivate init(revisionId: Data, plaintext: Data) { + self.revisionId = revisionId + self.plaintext = plaintext + } + var description: String { "OpenedContent([REDACTED])" } + var debugDescription: String { description } +} + +enum VaultContentCrypto { + static let maxPlaintextBytes = 16 * 1024 * 1024 - 400 + private static let payloadOverhead = 144 + private static let keyDomain = Data("zeron/content/key/v1\0".utf8) + private static let aadDomain = Data("zeron/content/aad/v1\0".utf8) + + static func seal( + binding: VaultRecordBinding, purpose: VaultContentPurpose, key: VaultContentKey, + signer: VaultDeviceSigner, plaintext: Data, maxPlaintextBytes: Int + ) throws -> VaultSealedContent { + try checked { + let limit = try payloadLimit(maxPlaintextBytes) + try checkScope(binding, key: key) + guard binding.authorId == signer.authorId else { throw VaultContentError.wrongAuthor } + guard plaintext.count <= maxPlaintextBytes else { throw VaultContentError.sizeLimitExceeded } + guard binding.membershipHash.count == 32 else { throw VaultRecordError.malformed } + let material = try randomBytes(48) + let revisionId = material.prefix(16) + let salt = material.suffix(32) + let header = protectedHeader(count: 5, purpose: purpose, identifier: key.identifier, salt: salt) + let context = try VaultRecordCodec.contextBytes(binding: binding, revisionId: revisionId) + let derived = try VaultCrypto.hkdfSHA256(inputKeyMaterial: key.material, salt: salt, info: keyDomain + context + header, outputByteCount: 32) + let box = try AES.GCM.seal(plaintext, using: derived, nonce: AES.GCM.Nonce(data: Data(repeating: 0, count: 12)), authenticating: aadDomain + context + header) + var payload = protectedHeader(count: 6, purpose: purpose, identifier: key.identifier, salt: salt) + VaultRecordCodec.bytesField(into: &payload, key: 5, value: box.ciphertext + box.tag) + let input = try VaultRecordCodec.signingBytes(binding: binding, revisionId: revisionId, payload: payload, maxPayloadBytes: limit) + let signature = try signer.key.signature(for: input) + let encoded = try VaultRecordCodec.encodeSigned(binding: binding, revisionId: revisionId, payload: payload, signature: signature, maxPayloadBytes: limit) + return VaultSealedContent(binding: binding, purpose: purpose, revisionId: revisionId, encoded: encoded) + } + } + + static func open( + _ encoded: Data, expected: VaultRecordBinding, purpose: VaultContentPurpose, + key: VaultContentKey, trustedPublicKey: Data, maxPlaintextBytes: Int + ) throws -> VaultOpenedContent { + try checked { + let limit = try payloadLimit(maxPlaintextBytes) + try checkScope(expected, key: key) + let record = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: limit).verify(expected: expected, trustedPublicKey: trustedPublicKey) + var reader = VaultRecordReader(record.payload) + guard try reader.argument(major: 5) == 6, try reader.uintField(0) == 1 else { throw VaultContentError.unsupportedFormat } + guard try reader.uintField(1) == 1 else { throw VaultContentError.unsupportedSuite } + guard let storedPurpose = VaultContentPurpose(rawValue: try reader.uintField(2)) else { throw VaultContentError.unsupportedPurpose } + guard storedPurpose == purpose else { throw VaultContentError.wrongPurpose } + let identifier = try reader.fixedField(3, count: 16) + guard identifier == key.identifier else { throw VaultContentError.wrongKey } + let salt = try reader.fixedField(4, count: 32) + let ciphertext = try reader.bytesField(5, limit: maxPlaintextBytes + 16) + guard reader.isAtEnd else { throw VaultRecordError.malformed } + guard ciphertext.count >= 16 else { throw VaultContentError.unsupportedFormat } + let header = protectedHeader(count: 5, purpose: purpose, identifier: identifier, salt: salt) + let context = try VaultRecordCodec.contextBytes(binding: expected, revisionId: record.revisionId) + let derived = try VaultCrypto.hkdfSHA256(inputKeyMaterial: key.material, salt: salt, info: keyDomain + context + header, outputByteCount: 32) + let plaintext = try VaultCrypto.openAES256GCM(key: derived, nonce: Data(repeating: 0, count: 12), aad: aadDomain + context + header, ciphertextAndTag: ciphertext, maxPlaintextBytes: maxPlaintextBytes) + return VaultOpenedContent(revisionId: record.revisionId, plaintext: plaintext) + } + } + + private static func checkScope(_ binding: VaultRecordBinding, key: VaultContentKey) throws { + guard binding.kind == .content else { throw VaultContentError.wrongKind } + guard key.scope == VaultKeyScope(binding) else { throw VaultContentError.wrongScope } + } + + private static func payloadLimit(_ maximum: Int) throws -> Int { + guard maximum >= 0, maximum <= maxPlaintextBytes else { throw VaultContentError.sizeLimitExceeded } + return maximum + payloadOverhead + } + + private static func protectedHeader(count: UInt64, purpose: VaultContentPurpose, identifier: Data, salt: Data) -> Data { + var header = Data(capacity: 128) + VaultRecordCodec.argument(into: &header, major: 5, value: count) + VaultRecordCodec.uintField(into: &header, key: 0, value: 1) + VaultRecordCodec.uintField(into: &header, key: 1, value: 1) + VaultRecordCodec.uintField(into: &header, key: 2, value: purpose.rawValue) + VaultRecordCodec.bytesField(into: &header, key: 3, value: identifier) + VaultRecordCodec.bytesField(into: &header, key: 4, value: salt) + return header + } + + static func randomBytes(_ count: Int) throws -> Data { + var bytes = Data(count: count) + let status = bytes.withUnsafeMutableBytes { buffer -> OSStatus in + guard let address = buffer.baseAddress else { return errSecParam } + return SecRandomCopyBytes(kSecRandomDefault, count, address) + } + guard status == errSecSuccess else { throw VaultContentError.entropyUnavailable } + return bytes + } + + private static func checked(_ operation: () throws -> Value) throws -> Value { + do { return try operation() } + catch let error as VaultContentError { throw error } + catch let error as VaultRecordError { throw VaultContentError.record(error) } + catch let error as VaultCryptoError { throw VaultContentError.crypto(error) } + catch { throw VaultContentError.cryptographyFailed } + } +} diff --git a/apps/ios/Zeron/Sync/VaultCrypto.swift b/apps/ios/Zeron/Sync/VaultCrypto.swift new file mode 100644 index 000000000..58d963de4 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultCrypto.swift @@ -0,0 +1,92 @@ +import CryptoKit +import Foundation + +enum VaultCryptoError: Error, Equatable { + case invalidKeyLength + case invalidNonceLength + case invalidCiphertextLength + case invalidSignatureLength + case invalidOutputLength + case sizeLimitExceeded + case authenticationFailed +} + +enum VaultCrypto { + static func openAES256GCM( + key: SymmetricKey, nonce: Data, aad: Data, + ciphertextAndTag: Data, maxPlaintextBytes: Int + ) throws -> Data { + guard key.bitCount == 256 else { throw VaultCryptoError.invalidKeyLength } + guard nonce.count == 12 else { throw VaultCryptoError.invalidNonceLength } + guard ciphertextAndTag.count >= 16 else { throw VaultCryptoError.invalidCiphertextLength } + guard ciphertextAndTag.count - 16 <= maxPlaintextBytes else { throw VaultCryptoError.sizeLimitExceeded } + do { + let box = try AES.GCM.SealedBox( + nonce: AES.GCM.Nonce(data: nonce), + ciphertext: ciphertextAndTag.dropLast(16), + tag: ciphertextAndTag.suffix(16) + ) + return try AES.GCM.open(box, using: key, authenticating: aad) + } catch { + throw VaultCryptoError.authenticationFailed + } + } + + static func verifyEd25519(publicKey: Data, message: Data, signature: Data) throws { + guard publicKey.count == 32 else { throw VaultCryptoError.invalidKeyLength } + guard signature.count == 64 else { throw VaultCryptoError.invalidSignatureLength } + guard passesEd25519PointEncodingPrecheck(publicKey), + passesEd25519PointEncodingPrecheck(signature.prefix(32)), + passesEd25519ScalarEncodingPrecheck(signature.suffix(32)) else { + throw VaultCryptoError.authenticationFailed + } + do { + let key = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey) + guard key.isValidSignature(signature, for: message) else { throw VaultCryptoError.authenticationFailed } + } catch { + throw VaultCryptoError.authenticationFailed + } + } + + private static let ed25519FieldModulus: [UInt8] = [0xed] + Array(repeating: 0xff, count: 30) + [0x7f] + + private static let ed25519SmallOrderY: [[UInt8]] = [ + Array(repeating: 0, count: 32), + [1] + Array(repeating: 0, count: 31), + [0xec] + Array(repeating: 0xff, count: 30) + [0x7f], + [ + 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0, + 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05, + ], + [ + 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f, + 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a, + ], + ] + + private static let ed25519ScalarOrder: [UInt8] = [ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, + ] + + static func passesEd25519PointEncodingPrecheck(_ encoded: Data) -> Bool { + guard encoded.count == 32 else { return false } + var y = Array(encoded) + y[31] &= 0x7f + return y.reversed().lexicographicallyPrecedes(ed25519FieldModulus.reversed()) + && !ed25519SmallOrderY.contains(y) + } + + static func passesEd25519ScalarEncodingPrecheck(_ encoded: Data) -> Bool { + encoded.count == 32 && encoded.reversed().lexicographicallyPrecedes(ed25519ScalarOrder.reversed()) + } + + static func hkdfSHA256( + inputKeyMaterial: SymmetricKey, salt: Data, info: Data, outputByteCount: Int + ) throws -> SymmetricKey { + guard (1...8160).contains(outputByteCount) else { throw VaultCryptoError.invalidOutputLength } + return HKDF.deriveKey( + inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount + ) + } +} diff --git a/apps/ios/Zeron/Sync/VaultEnvelope.swift b/apps/ios/Zeron/Sync/VaultEnvelope.swift new file mode 100644 index 000000000..271aaf215 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultEnvelope.swift @@ -0,0 +1,343 @@ +import CryptoKit +import Foundation + +/// Keyrings, key envelopes, and the recovery kit (RFC 0001 §5, §7.8) — the +/// Swift twins of `keyring.rs`, `envelope.rs`, and `recovery.rs`. Keyring +/// envelopes are opened with CryptoKit's HPKE (X25519 / HKDF-SHA256 / +/// AES-256-GCM, base mode); object keys unwrap under an epoch key with the +/// same per-record-derived-key construction content records use. +enum VaultEnvelopeError: Error, Equatable { + case record(VaultRecordError) + case malformed + case unsupportedVersion + case unsupportedFormat + case wrongKind + case wrongRecipient + case wrongScope + case duplicateEpoch + case invalidEpoch + case tooManyEpochs + case sizeLimitExceeded + case cryptographyFailed +} + +/// The workspace keyring: one 32-byte wrapping key per write epoch. +struct VaultKeyring: Equatable { + static let maxEpochs = 1024 + static let maxBytes = 16 + maxEpochs * 44 + + private(set) var epochs: [UInt64: Data] = [:] + + init() {} + + mutating func insert(epoch: UInt64, key: Data) throws { + guard epoch > 0 else { throw VaultEnvelopeError.invalidEpoch } + guard key.count == 32 else { throw VaultEnvelopeError.malformed } + if let existing = epochs[epoch] { + guard existing == key else { throw VaultEnvelopeError.duplicateEpoch } + return + } + guard epochs.count < Self.maxEpochs else { throw VaultEnvelopeError.tooManyEpochs } + epochs[epoch] = key + } + + mutating func merge(_ other: VaultKeyring) throws { + for (epoch, key) in other.epochs { try insert(epoch: epoch, key: key) } + } + + func epochKey(_ epoch: UInt64) -> Data? { epochs[epoch] } + var latestEpoch: UInt64? { epochs.keys.max() } + + func encode() -> Data { + var out = Data(capacity: 16 + epochs.count * 44) + VaultRecordCodec.argument(into: &out, major: 5, value: 2) + VaultRecordCodec.uintField(into: &out, key: 0, value: 1) + VaultRecordCodec.argument(into: &out, major: 0, value: 1) + VaultRecordCodec.argument(into: &out, major: 4, value: UInt64(epochs.count)) + for epoch in epochs.keys.sorted() { + VaultRecordCodec.argument(into: &out, major: 4, value: 2) + VaultRecordCodec.argument(into: &out, major: 0, value: epoch) + VaultRecordCodec.argument(into: &out, major: 2, value: 32) + out.append(epochs[epoch]!) + } + return out + } + + static func decode(_ bytes: Data) throws -> VaultKeyring { + guard bytes.count <= maxBytes else { throw VaultEnvelopeError.tooManyEpochs } + return try VaultEnvelope.checked { + var reader = VaultRecordReader(bytes) + guard try reader.argument(major: 5) == 2 else { throw VaultEnvelopeError.malformed } + guard try reader.uintField(0) == 1 else { throw VaultEnvelopeError.unsupportedVersion } + guard try reader.argument(major: 0) == 1 else { throw VaultEnvelopeError.malformed } + let count = try reader.argument(major: 4) + guard count <= UInt64(maxEpochs) else { throw VaultEnvelopeError.tooManyEpochs } + var keyring = VaultKeyring() + var previous: UInt64 = 0 + for _ in 0.. previous else { throw VaultEnvelopeError.malformed } + previous = epoch + keyring.epochs[epoch] = try reader.fixedBytes(32) + } + guard reader.isAtEnd else { throw VaultEnvelopeError.malformed } + return keyring + } + } +} + +enum VaultRecipientKind: UInt64 { + case device = 1, recovery = 2, epoch = 3 +} + +enum VaultEnvelope { + static let maxPayloadBytes = VaultKeyring.maxBytes + 16 + 128 + private static let keyringInfoDomain = Data("zeron/keyring-envelope/v1\0".utf8) + private static let objectKeyDomain = Data("zeron/object-key/v1\0".utf8) + private static let objectAadDomain = Data("zeron/object-key/aad/v1\0".utf8) + private static let hpkeSuite = HPKE.Ciphersuite(kem: .Curve25519_HKDF_SHA256, kdf: .HKDF_SHA256, aead: .AES_GCM_256) + + static func epochRecipientId(_ epoch: UInt64) -> Data { + var out = Data(repeating: 0, count: 8) + var bigEndian = epoch.bigEndian + withUnsafeBytes(of: &bigEndian) { out.append(contentsOf: $0) } + return out + } + + private struct ParsedPayload { + let recipientKind: VaultRecipientKind + let recipientId: Data + let encapsulation: Data + let ciphertext: Data + } + + private static func parsePayload(_ payload: Data, maxPlaintext: Int) throws -> ParsedPayload { + var reader = VaultRecordReader(payload) + guard try reader.argument(major: 5) == 5, try reader.uintField(0) == 1 else { + throw VaultEnvelopeError.unsupportedFormat + } + guard let kind = VaultRecipientKind(rawValue: try reader.uintField(1)) else { + throw VaultEnvelopeError.unsupportedFormat + } + let recipientId = try reader.fixedField(2, count: 16) + let encapsulation = try reader.fixedField(3, count: 32) + let ciphertext = try reader.bytesField(4, limit: maxPlaintext + 16) + guard reader.isAtEnd, ciphertext.count >= 16 else { throw VaultEnvelopeError.unsupportedFormat } + return ParsedPayload(recipientKind: kind, recipientId: recipientId, encapsulation: encapsulation, ciphertext: ciphertext) + } + + private static func header(count: UInt64, kind: VaultRecipientKind, recipientId: Data) -> Data { + var out = Data(capacity: 96) + VaultRecordCodec.argument(into: &out, major: 5, value: count) + VaultRecordCodec.uintField(into: &out, key: 0, value: 1) + VaultRecordCodec.uintField(into: &out, key: 1, value: kind.rawValue) + VaultRecordCodec.bytesField(into: &out, key: 2, value: recipientId) + return out + } + + /// Verify and open a keyring envelope addressed to this recipient. + static func openKeyring( + _ encoded: Data, expected: VaultRecordBinding, recipientKind: VaultRecipientKind, + recipientId: Data, recipientKey: Curve25519.KeyAgreement.PrivateKey, trustedPublicKey: Data + ) throws -> VaultKeyring { + try checked { + guard expected.kind == .keyEnvelope else { throw VaultEnvelopeError.wrongKind } + let record = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: maxPayloadBytes) + .verify(expected: expected, trustedPublicKey: trustedPublicKey) + let parsed = try parsePayload(record.payload, maxPlaintext: VaultKeyring.maxBytes + 16) + guard parsed.recipientKind == recipientKind, parsed.recipientId == recipientId, + parsed.recipientKind != .epoch else { throw VaultEnvelopeError.wrongRecipient } + let context = try VaultRecordCodec.contextBytes(binding: expected, revisionId: record.revisionId) + let info = keyringInfoDomain + context + header(count: 3, kind: parsed.recipientKind, recipientId: parsed.recipientId) + var recipient = try HPKE.Recipient(privateKey: recipientKey, ciphersuite: hpkeSuite, + info: info, encapsulatedKey: parsed.encapsulation) + let plaintext = try recipient.open(parsed.ciphertext, authenticating: Data()) + return try VaultKeyring.decode(plaintext) + } + } + + /// Verify and unwrap an object key envelope with the epoch key named by + /// the expected binding. + static func unwrapObjectKey( + _ encoded: Data, expected: VaultRecordBinding, epochKey: Data, trustedPublicKey: Data + ) throws -> VaultContentKey { + try checked { + guard expected.kind == .keyEnvelope else { throw VaultEnvelopeError.wrongKind } + guard epochKey.count == 32 else { throw VaultEnvelopeError.malformed } + let record = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: maxPayloadBytes) + .verify(expected: expected, trustedPublicKey: trustedPublicKey) + let parsed = try parsePayload(record.payload, maxPlaintext: 48) + guard parsed.recipientKind == .epoch, parsed.recipientId == epochRecipientId(expected.epoch) else { + throw VaultEnvelopeError.wrongRecipient + } + let context = try VaultRecordCodec.contextBytes(binding: expected, revisionId: record.revisionId) + var prefix = header(count: 5, kind: .epoch, recipientId: parsed.recipientId) + VaultRecordCodec.bytesField(into: &prefix, key: 3, value: parsed.encapsulation) + prefix[prefix.startIndex] = 0xa4 // fields 0..3 as a length-4 map + let derived = try VaultCrypto.hkdfSHA256( + inputKeyMaterial: SymmetricKey(data: epochKey), salt: parsed.encapsulation, + info: objectKeyDomain + context + prefix, outputByteCount: 32 + ) + let plaintext = try VaultCrypto.openAES256GCM( + key: derived, nonce: Data(repeating: 0, count: 12), aad: objectAadDomain + context + prefix, + ciphertextAndTag: parsed.ciphertext, maxPlaintextBytes: 48 + ) + guard plaintext.count == 48 else { throw VaultEnvelopeError.unsupportedFormat } + return try VaultContentKey(scope: VaultKeyScope(expected), identifier: plaintext.prefix(16), + bytes: plaintext.suffix(32)) + } + } + + static func sign(binding: VaultRecordBinding, revision: Data, payload: Data, + signingKey: Curve25519.Signing.PrivateKey, limit: Int) throws -> Data { + let input = try VaultRecordCodec.signingBytes(binding: binding, revisionId: revision, payload: payload, maxPayloadBytes: limit) + return try VaultRecordCodec.encodeSigned(binding: binding, revisionId: revision, payload: payload, + signature: signingKey.signature(for: input), maxPayloadBytes: limit) + } + + static func sealKeyring(binding: VaultRecordBinding, kind: VaultRecipientKind, recipientId: Data, + recipientKey: Curve25519.KeyAgreement.PublicKey, keyring: VaultKeyring, + signingKey: Curve25519.Signing.PrivateKey) throws -> Data { + guard binding.kind == .keyEnvelope, kind != .epoch, recipientId.count == 16 else { throw VaultEnvelopeError.wrongKind } + let revision = try VaultContentCrypto.randomBytes(16) + let context = try VaultRecordCodec.contextBytes(binding: binding, revisionId: revision) + let info = keyringInfoDomain + context + header(count: 3, kind: kind, recipientId: recipientId) + var sender = try HPKE.Sender(recipientKey: recipientKey, ciphersuite: hpkeSuite, info: info) + let ciphertext = try sender.seal(keyring.encode(), authenticating: Data()) + var payload = header(count: 5, kind: kind, recipientId: recipientId) + VaultRecordCodec.bytesField(into: &payload, key: 3, value: sender.encapsulatedKey) + VaultRecordCodec.bytesField(into: &payload, key: 4, value: ciphertext) + return try sign(binding: binding, revision: revision, payload: payload, signingKey: signingKey, limit: maxPayloadBytes) + } + + static func wrapObjectKey(binding: VaultRecordBinding, epochKey: Data, key: VaultContentKey, + signingKey: Curve25519.Signing.PrivateKey) throws -> Data { + guard binding.kind == .keyEnvelope, epochKey.count == 32, + key.scope == VaultKeyScope(binding) else { throw VaultEnvelopeError.wrongScope } + let revision = try VaultContentCrypto.randomBytes(16) + let salt = try VaultContentCrypto.randomBytes(32) + let context = try VaultRecordCodec.contextBytes(binding: binding, revisionId: revision) + var prefix = header(count: 4, kind: .epoch, recipientId: epochRecipientId(binding.epoch)) + VaultRecordCodec.bytesField(into: &prefix, key: 3, value: salt) + let derived = try VaultCrypto.hkdfSHA256(inputKeyMaterial: SymmetricKey(data: epochKey), salt: salt, + info: objectKeyDomain + context + prefix, outputByteCount: 32) + let box = try AES.GCM.seal(key.identifier + key.exposeSecret(), using: derived, + nonce: AES.GCM.Nonce(data: Data(repeating: 0, count: 12)), + authenticating: objectAadDomain + context + prefix) + var payload = prefix + payload[payload.startIndex] = 0xa5 + VaultRecordCodec.bytesField(into: &payload, key: 4, value: box.ciphertext + box.tag) + return try sign(binding: binding, revision: revision, payload: payload, signingKey: signingKey, limit: maxPayloadBytes) + } + + fileprivate static func checked(_ operation: () throws -> Value) throws -> Value { + do { return try operation() } + catch let error as VaultEnvelopeError { throw error } + catch let error as VaultRecordError { throw VaultEnvelopeError.record(error) } + catch let error as VaultContentError { _ = error; throw VaultEnvelopeError.wrongScope } + catch { throw VaultEnvelopeError.cryptographyFailed } + } +} + +enum VaultRecoveryError: Error, Equatable { + case invalidCharacter + case invalidLength + case checksumMismatch + case derivationFailed +} + +/// The recovery secret and its kit text (RFC §4.1; plan Q4-A). +struct VaultRecoverySecret { + private static let kitDomain = Data("zeron/recovery-kit/v1\0".utf8) + private static let signingLabel = Data("zeron/recovery/sign/v1".utf8) + private static let encryptionLabel = Data("zeron/recovery/hpke/v1".utf8) + private static let alphabet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".utf8) + private static let kitSymbols = 55 + + let secret: Data + + init(secret: Data) throws { + guard secret.count == 32 else { throw VaultRecoveryError.invalidLength } + self.secret = secret + } + + private var checksum: Data { Data(SHA256.hash(data: Self.kitDomain + secret).prefix(2)) } + + var kit: String { + let symbols = Self.base32Encode(secret + checksum) + var text = "" + for (index, symbol) in symbols.enumerated() { + if index > 0, index % 5 == 0 { text.append("-") } + text.append(Character(UnicodeScalar(symbol))) + } + return text + } + + init(kit: String) throws { + var symbols: [UInt8] = [] + for scalar in kit.unicodeScalars { + if scalar.properties.isWhitespace || scalar == "-" { continue } + guard scalar.isASCII else { throw VaultRecoveryError.invalidCharacter } + symbols.append(UInt8(ascii: Unicode.Scalar(String(scalar).uppercased())!)) + } + guard symbols.count == Self.kitSymbols else { throw VaultRecoveryError.invalidLength } + let payload = try Self.base32Decode(symbols) + let secret = payload.prefix(32) + try self.init(secret: Data(secret)) + guard payload.suffix(from: payload.startIndex + 32).prefix(2) == checksum else { + throw VaultRecoveryError.checksumMismatch + } + } + + func signingKey() throws -> Curve25519.Signing.PrivateKey { + let seed = try VaultCrypto.hkdfSHA256(inputKeyMaterial: SymmetricKey(data: secret), salt: Data(), + info: Self.signingLabel, outputByteCount: 32) + do { return try Curve25519.Signing.PrivateKey(rawRepresentation: seed.withUnsafeBytes { Data($0) }) } + catch { throw VaultRecoveryError.derivationFailed } + } + + func encryptionKey() throws -> Curve25519.KeyAgreement.PrivateKey { + let seed = try VaultCrypto.hkdfSHA256(inputKeyMaterial: SymmetricKey(data: secret), salt: Data(), + info: Self.encryptionLabel, outputByteCount: 32) + do { return try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: seed.withUnsafeBytes { Data($0) }) } + catch { throw VaultRecoveryError.derivationFailed } + } + + func authorityId() throws -> Data { + VaultPolicy.recoveryAuthorityId(recoverySigningKey: try signingKey().publicKey.rawRepresentation) + } + + private static func base32Encode(_ bytes: Data) -> [UInt8] { + var out: [UInt8] = [] + var buffer: UInt32 = 0 + var bits = 0 + for byte in bytes { + buffer = (buffer << 8) | UInt32(byte) + bits += 8 + while bits >= 5 { + bits -= 5 + out.append(alphabet[Int((buffer >> UInt32(bits)) & 31)]) + } + } + if bits > 0 { out.append(alphabet[Int((buffer << UInt32(5 - bits)) & 31)]) } + return out + } + + private static func base32Decode(_ symbols: [UInt8]) throws -> Data { + var out = Data() + var buffer: UInt32 = 0 + var bits = 0 + for symbol in symbols { + guard let value = alphabet.firstIndex(of: symbol) else { throw VaultRecoveryError.invalidCharacter } + buffer = (buffer << 5) | UInt32(value) + bits += 5 + if bits >= 8 { + bits -= 8 + out.append(UInt8((buffer >> UInt32(bits)) & 0xff)) + } + } + if bits > 0, buffer & ((1 << UInt32(bits)) - 1) != 0 { throw VaultRecoveryError.invalidCharacter } + return out + } +} diff --git a/apps/ios/Zeron/Sync/VaultPersistence.swift b/apps/ios/Zeron/Sync/VaultPersistence.swift new file mode 100644 index 000000000..f87cdd4a5 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultPersistence.swift @@ -0,0 +1,130 @@ +import CryptoKit +import Foundation +import Security +import Darwin + +enum VaultStorageError: Error, Equatable { + case keychain(OSStatus) + case keyUnavailable + case invalidState + case tooLarge + case io(Int32) +} + +protocol VaultSecretStorage: Sendable { + func load(account: String) throws -> Data? + func insert(account: String, value: Data) throws +} + +struct VaultKeychainStorage: VaultSecretStorage { + private func query(_ account: String) -> [String: Any] { + [kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: "sh.zeron.ios.vault", + kSecAttrAccount as String: account, + kSecAttrSynchronizable as String: false] + } + + func load(account: String) throws -> Data? { + var query = query(account) + query[kSecReturnData as String] = true + query[kSecMatchLimit as String] = kSecMatchLimitOne + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess else { throw VaultStorageError.keychain(status) } + guard let data = result as? Data, data.count == 32 else { throw VaultStorageError.invalidState } + return data + } + + func insert(account: String, value: Data) throws { + guard value.count == 32 else { throw VaultStorageError.invalidState } + var query = query(account) + query[kSecValueData as String] = value + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess || status == errSecDuplicateItem else { + throw VaultStorageError.keychain(status) + } + } +} + +struct VaultPersistence: Sendable { + static let maxBytes = 8 * 1024 * 1024 + let directory: URL + let account: String + let secrets: any VaultSecretStorage + + init(origin: URL, orgId: String, userId: String, directory: URL? = nil, + secrets: any VaultSecretStorage = VaultKeychainStorage()) { + account = Data(SHA256.hash(data: Data("zeron/ios/store/v1\0\(origin.absoluteString)\0\(orgId)\0\(userId)".utf8))).vaultHex + self.directory = directory ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("ZeronVault", isDirectory: true).appendingPathComponent(account, isDirectory: true) + self.secrets = secrets + } + + var stateURL: URL { directory.appendingPathComponent("state.enc") } + var exists: Bool { + var info = stat() + return lstat(stateURL.path, &info) == 0 || errno != ENOENT + } + private var aad: Data { Data("zeron/ios/vault-state/v1\0\(account)".utf8) } + + func load() throws -> Data? { + guard exists else { return nil } + guard let key = try secrets.load(account: account), key.count == 32 else { throw VaultStorageError.keyUnavailable } + let values = try stateURL.resourceValues(forKeys: [.fileSizeKey, .isSymbolicLinkKey]) + guard values.isSymbolicLink != true else { throw VaultStorageError.invalidState } + guard let size = values.fileSize, size <= Self.maxBytes else { throw VaultStorageError.tooLarge } + let encoded = try Data(contentsOf: stateURL) + guard encoded.first == 1 else { throw VaultStorageError.invalidState } + return try AES.GCM.open(AES.GCM.SealedBox(combined: encoded.dropFirst()), + using: SymmetricKey(data: key), authenticating: aad) + } + + func save(_ data: Data) throws { + guard data.count <= Self.maxBytes - 64 else { throw VaultStorageError.tooLarge } + if try secrets.load(account: account) == nil { + guard !exists else { throw VaultStorageError.keyUnavailable } + try secrets.insert(account: account, value: SymmetricKey(size: .bits256).withUnsafeBytes { Data($0) }) + } + guard let key = try secrets.load(account: account), key.count == 32 else { throw VaultStorageError.keyUnavailable } + let box = try AES.GCM.seal(data, using: SymmetricKey(data: key), authenticating: aad) + guard let combined = box.combined else { throw VaultStorageError.invalidState } + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + var location = directory + var values = URLResourceValues() + values.isExcludedFromBackup = true + try location.setResourceValues(values) + try Self.writeDurably(Data([1]) + combined, to: stateURL) + } + + static func writeDurably(_ data: Data, to url: URL) throws { + var options: Data.WritingOptions = [.atomic] + #if os(iOS) + options.insert(.completeFileProtectionUntilFirstUserAuthentication) + #endif + try data.write(to: url, options: options) + let file = try FileHandle(forWritingTo: url) + defer { try? file.close() } + try file.synchronize() + let descriptor = Darwin.open(url.deletingLastPathComponent().path, O_RDONLY | O_CLOEXEC) + guard descriptor >= 0 else { throw VaultStorageError.io(errno) } + defer { Darwin.close(descriptor) } + guard fsync(descriptor) == 0 else { throw VaultStorageError.io(errno) } + } +} + +extension Data { + var vaultHex: String { map { String(format: "%02x", $0) }.joined() } + + init?(vaultHex: String, count: Int) { + let bytes = Array(vaultHex.utf8) + guard bytes.count == count * 2, bytes.allSatisfy({ $0 < 128 }) else { return nil } + var output = Data(capacity: count) + for index in stride(from: 0, to: bytes.count, by: 2) { + guard let value = UInt8(String(decoding: bytes[index..<(index + 2)], as: UTF8.self), radix: 16) else { return nil } + output.append(value) + } + self = output + } +} diff --git a/apps/ios/Zeron/Sync/VaultPolicy.swift b/apps/ios/Zeron/Sync/VaultPolicy.swift new file mode 100644 index 000000000..c32c3ab62 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultPolicy.swift @@ -0,0 +1,382 @@ +import CryptoKit +import Foundation + +/// Signed membership policy records (RFC 0001 §5, §7.8) — the Swift twin of +/// `crates/crypto/src/policy.rs`. A verified `VaultMembershipState` is the +/// only source of "who may sign for this vault": every content/envelope +/// verification takes its expected binding and the author's public key from +/// here, never from the record being checked. +enum VaultPolicyError: Error, Equatable { + case record(VaultRecordError) + case malformed + case unsupportedVersion + case unsupportedOperation + case wrongVault + case wrongProfile + case wrongSequence + case wrongParent + case wrongEpoch + case unknownAuthor + case revokedAuthor + case invalidDeviceSet + case invalidRecoveryKeys + case tooManyDevices + case invalidTransition +} + +enum VaultDeviceStatus: UInt64 { + case active = 0 + case revoked = 1 +} + +enum VaultPolicyOperation: UInt64 { + case genesis = 1, addDevice = 2, revokeDevice = 3, rotateRecovery = 4, recoveryTransition = 5 +} + +struct VaultDeviceEntry: Equatable { + var deviceId: Data + var signingKey: Data + var encryptionKey: Data + var status: VaultDeviceStatus +} + +struct VaultPolicyPayload: Equatable { + var sequence: UInt64 + var parentHash: Data + var profileHash: Data + var epoch: UInt64 + var operation: VaultPolicyOperation + var recoverySigningKey: Data + var recoveryEncryptionKey: Data + var devices: [VaultDeviceEntry] + + static let maxDevices = 64 + + static func decode(_ bytes: Data) throws -> VaultPolicyPayload { + try VaultPolicy.checked { + var reader = VaultRecordReader(bytes) + guard try reader.argument(major: 5) == 9 else { throw VaultPolicyError.malformed } + guard try reader.uintField(0) == 1 else { throw VaultPolicyError.unsupportedVersion } + let sequence = try reader.uintField(1) + let parentHash = try reader.fixedField(2, count: 32) + let profileHash = try reader.fixedField(3, count: 32) + let epoch = try reader.uintField(4) + guard let operation = VaultPolicyOperation(rawValue: try reader.uintField(5)) else { + throw VaultPolicyError.unsupportedOperation + } + let recoverySigningKey = try reader.fixedField(6, count: 32) + let recoveryEncryptionKey = try reader.fixedField(7, count: 32) + guard try reader.argument(major: 0) == 8 else { throw VaultPolicyError.malformed } + let count = try reader.argument(major: 4) + guard count <= UInt64(maxDevices) else { throw VaultPolicyError.tooManyDevices } + var devices: [VaultDeviceEntry] = [] + for _ in 0.. Data { + guard devices.count <= Self.maxDevices else { throw VaultPolicyError.tooManyDevices } + var out = Data(capacity: 256 + devices.count * 96) + VaultRecordCodec.argument(into: &out, major: 5, value: 9) + VaultRecordCodec.uintField(into: &out, key: 0, value: 1) + VaultRecordCodec.uintField(into: &out, key: 1, value: sequence) + VaultRecordCodec.bytesField(into: &out, key: 2, value: parentHash) + VaultRecordCodec.bytesField(into: &out, key: 3, value: profileHash) + VaultRecordCodec.uintField(into: &out, key: 4, value: epoch) + VaultRecordCodec.uintField(into: &out, key: 5, value: operation.rawValue) + VaultRecordCodec.bytesField(into: &out, key: 6, value: recoverySigningKey) + VaultRecordCodec.bytesField(into: &out, key: 7, value: recoveryEncryptionKey) + VaultRecordCodec.argument(into: &out, major: 0, value: 8) + VaultRecordCodec.argument(into: &out, major: 4, value: UInt64(devices.count)) + for device in devices { + VaultRecordCodec.argument(into: &out, major: 4, value: 4) + VaultRecordCodec.argument(into: &out, major: 2, value: 16) + out.append(device.deviceId) + VaultRecordCodec.argument(into: &out, major: 2, value: 32) + out.append(device.signingKey) + VaultRecordCodec.argument(into: &out, major: 2, value: 32) + out.append(device.encryptionKey) + VaultRecordCodec.argument(into: &out, major: 0, value: device.status.rawValue) + } + return out + } +} + +/// An enrollment request (RFC §6.2): the pending device's public identity +/// bound to the vault it wants to join, with a key-possession proof. +struct VaultEnrollmentRequest: Equatable { + var vaultId: Data + var requestId: Data + var deviceId: Data + var signingKey: Data + var encryptionKey: Data + + private var body: Data { vaultId + requestId + deviceId + signingKey + encryptionKey } + + var proofInput: Data { VaultPolicy.enrollDomain + body } + + func verify(proof: Data) throws { + guard VaultCrypto.passesEd25519PointEncodingPrecheck(signingKey), + !encryptionKey.allSatisfy({ $0 == 0 }), + deviceId != VaultPolicy.policyObjectId else { + throw VaultPolicyError.invalidDeviceSet + } + try VaultCrypto.verifyEd25519(publicKey: signingKey, message: proofInput, signature: proof) + } + + /// "NNNN-NNNN": both sides derive it from the request they hold PLUS the + /// genesis hash of the vault they see (RFC §7.8). + func pairingCode(genesisHash: Data) -> String { + let digest = SHA256.hash(data: VaultPolicy.pairingDomain + body + genesisHash) + let bytes = Array(digest.prefix(4)) + let value = (UInt32(bytes[0]) << 24 | UInt32(bytes[1]) << 16 | UInt32(bytes[2]) << 8 | UInt32(bytes[3])) + % 100_000_000 + return String(format: "%04d-%04d", value / 10_000, value % 10_000) + } + + var deviceEntry: VaultDeviceEntry { + VaultDeviceEntry(deviceId: deviceId, signingKey: signingKey, encryptionKey: encryptionKey, status: .active) + } +} + +enum VaultPolicy { + static let policyObjectId = Data(repeating: 0, count: 16) + static let maxPolicyBytes = 64 * 1024 + fileprivate static let membershipDomain = Data("zeron/membership/v1\0".utf8) + fileprivate static let profileDomain = Data("zeron/profile/v1\0".utf8) + fileprivate static let recoveryIdDomain = Data("zeron/recovery-id/v1\0".utf8) + fileprivate static let enrollDomain = Data("zeron/enroll/v1\0".utf8) + fileprivate static let pairingDomain = Data("zeron/pairing-code/v1\0".utf8) + + static func profileHash(orgId: String, userId: String) -> Data { + Data(SHA256.hash(data: profileDomain + Data(orgId.utf8) + Data([0]) + Data(userId.utf8))) + } + + static func membershipHash(_ record: Data) -> Data { + Data(SHA256.hash(data: membershipDomain + record)) + } + + static func recoveryAuthorityId(recoverySigningKey: Data) -> Data { + Data(SHA256.hash(data: recoveryIdDomain + recoverySigningKey).prefix(16)) + } + + static func policyBinding(vaultId: Data, generation: Data, epoch: UInt64, authorId: Data, parent: Data) -> VaultRecordBinding { + VaultRecordBinding(kind: .policy, vaultId: vaultId, generation: generation, epoch: epoch, + objectId: policyObjectId, authorId: authorId, membershipHash: parent) + } + + fileprivate static func checked(_ operation: () throws -> Value) throws -> Value { + do { return try operation() } + catch let error as VaultPolicyError { throw error } + catch let error as VaultRecordError { throw VaultPolicyError.record(error) } + catch { throw VaultPolicyError.malformed } + } + + fileprivate static func validDeviceEntries(_ devices: [VaultDeviceEntry]) -> Bool { + guard !devices.isEmpty, devices.count <= VaultPolicyPayload.maxDevices else { return false } + for (index, device) in devices.enumerated() { + guard VaultCrypto.passesEd25519PointEncodingPrecheck(device.signingKey), + !device.encryptionKey.allSatisfy({ $0 == 0 }), + device.deviceId != policyObjectId else { return false } + for other in devices[.. Bool { + VaultCrypto.passesEd25519PointEncodingPrecheck(payload.recoverySigningKey) + && !payload.recoveryEncryptionKey.allSatisfy({ $0 == 0 }) + && !payload.devices.contains { + $0.signingKey == payload.recoverySigningKey || $0.encryptionKey == payload.recoveryEncryptionKey + } + } +} + +/// A verified membership head: the trust anchor for every other record. +struct VaultMembershipState: Equatable { + let vaultId: Data + let generation: Data + let genesisHash: Data + let hash: Data + let sequence: UInt64 + let epoch: UInt64 + let profileHash: Data + let recoverySigningKey: Data + let recoveryEncryptionKey: Data + let devices: [VaultDeviceEntry] + + var recoveryAuthorityId: Data { VaultPolicy.recoveryAuthorityId(recoverySigningKey: recoverySigningKey) } + + func device(_ id: Data) -> VaultDeviceEntry? { devices.first { $0.deviceId == id } } + func activeDevice(_ id: Data) -> VaultDeviceEntry? { + device(id).flatMap { $0.status == .active ? $0 : nil } + } + + /// Pin a genesis record against LOCALLY expected identity (never the + /// server's descriptor alone). + static func fromGenesis(_ encoded: Data, expectedVaultId: Data, expectedGeneration: Data, + expectedProfileHash: Data) throws -> VaultMembershipState { + try VaultPolicy.checked { + let parsed = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: VaultPolicy.maxPolicyBytes) + let untrusted = parsed.untrustedBinding + guard untrusted.kind == .policy, untrusted.vaultId == expectedVaultId, + untrusted.generation == expectedGeneration, untrusted.objectId == VaultPolicy.policyObjectId else { + throw VaultPolicyError.wrongVault + } + let payload = try VaultPolicyPayload.decode(try payloadOf(encoded)) + guard payload.operation == .genesis else { throw VaultPolicyError.invalidTransition } + guard payload.sequence == 0, payload.parentHash.allSatisfy({ $0 == 0 }) else { throw VaultPolicyError.wrongSequence } + guard payload.profileHash == expectedProfileHash else { throw VaultPolicyError.wrongProfile } + guard payload.epoch == 1 else { throw VaultPolicyError.wrongEpoch } + guard VaultPolicy.validDeviceEntries(payload.devices), payload.devices.count == 1, + let device = payload.devices.first, device.status == .active else { + throw VaultPolicyError.invalidDeviceSet + } + guard VaultPolicy.validRecoveryKeys(payload) else { throw VaultPolicyError.invalidRecoveryKeys } + let expected = VaultPolicy.policyBinding(vaultId: expectedVaultId, generation: expectedGeneration, + epoch: 1, authorId: device.deviceId, + parent: Data(repeating: 0, count: 32)) + _ = try parsed.verify(expected: expected, trustedPublicKey: device.signingKey) + let hash = VaultPolicy.membershipHash(encoded) + return VaultMembershipState( + vaultId: expectedVaultId, generation: expectedGeneration, genesisHash: hash, hash: hash, + sequence: 0, epoch: 1, profileHash: payload.profileHash, + recoverySigningKey: payload.recoverySigningKey, + recoveryEncryptionKey: payload.recoveryEncryptionKey, devices: payload.devices + ) + } + } + + /// Verify and apply the next record in the history. + func apply(_ encoded: Data) throws -> VaultMembershipState { + try VaultPolicy.checked { + let parsed = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: VaultPolicy.maxPolicyBytes) + let untrusted = parsed.untrustedBinding + guard untrusted.kind == .policy, untrusted.vaultId == vaultId, untrusted.generation == generation, + untrusted.objectId == VaultPolicy.policyObjectId else { throw VaultPolicyError.wrongVault } + guard untrusted.membershipHash == hash else { throw VaultPolicyError.wrongParent } + let payload = try VaultPolicyPayload.decode(try Self.payloadOf(encoded)) + guard sequence < UInt64.max, payload.sequence == sequence + 1 else { throw VaultPolicyError.wrongSequence } + guard payload.parentHash == hash else { throw VaultPolicyError.wrongParent } + guard payload.profileHash == profileHash else { throw VaultPolicyError.wrongProfile } + guard VaultPolicy.validDeviceEntries(payload.devices) else { throw VaultPolicyError.invalidDeviceSet } + guard VaultPolicy.validRecoveryKeys(payload) else { throw VaultPolicyError.invalidRecoveryKeys } + guard epoch < UInt64.max else { throw VaultPolicyError.wrongEpoch } + let signingKey: Data + switch payload.operation { + case .genesis: + throw VaultPolicyError.invalidTransition + case .recoveryTransition: + guard untrusted.authorId == recoveryAuthorityId else { throw VaultPolicyError.unknownAuthor } + signingKey = recoverySigningKey + default: + guard let author = device(untrusted.authorId) else { throw VaultPolicyError.unknownAuthor } + guard author.status == .active else { throw VaultPolicyError.revokedAuthor } + signingKey = author.signingKey + } + let expectedEpoch = payload.operation == .addDevice ? epoch : epoch + 1 + guard payload.epoch == expectedEpoch, untrusted.epoch == expectedEpoch else { throw VaultPolicyError.wrongEpoch } + try checkTransition(payload) + let expected = VaultPolicy.policyBinding(vaultId: vaultId, generation: generation, epoch: expectedEpoch, + authorId: untrusted.authorId, parent: hash) + _ = try parsed.verify(expected: expected, trustedPublicKey: signingKey) + return VaultMembershipState( + vaultId: vaultId, generation: generation, genesisHash: genesisHash, + hash: VaultPolicy.membershipHash(encoded), sequence: payload.sequence, epoch: payload.epoch, + profileHash: profileHash, recoverySigningKey: payload.recoverySigningKey, + recoveryEncryptionKey: payload.recoveryEncryptionKey, devices: payload.devices + ) + } + } + + private func checkTransition(_ payload: VaultPolicyPayload) throws { + let recoveryUnchanged = payload.recoverySigningKey == recoverySigningKey + && payload.recoveryEncryptionKey == recoveryEncryptionKey + let recoveryReplaced = payload.recoverySigningKey != recoverySigningKey + && payload.recoveryEncryptionKey != recoveryEncryptionKey + let sameDevice: (VaultDeviceEntry, VaultDeviceEntry) -> Bool = { + $0.deviceId == $1.deviceId && $0.signingKey == $1.signingKey && $0.encryptionKey == $1.encryptionKey + } + func prefix(_ accept: (VaultDeviceEntry, VaultDeviceEntry) -> Bool) throws { + guard payload.devices.count >= devices.count else { throw VaultPolicyError.invalidDeviceSet } + for (previous, next) in zip(devices, payload.devices) where !accept(previous, next) { + throw VaultPolicyError.invalidDeviceSet + } + } + let added = Array(payload.devices.dropFirst(devices.count)) + switch payload.operation { + case .genesis: + throw VaultPolicyError.invalidTransition + case .addDevice: + guard recoveryUnchanged else { throw VaultPolicyError.invalidRecoveryKeys } + try prefix { $0 == $1 } + guard added.count == 1, added[0].status == .active else { throw VaultPolicyError.invalidDeviceSet } + case .revokeDevice: + guard recoveryUnchanged else { throw VaultPolicyError.invalidRecoveryKeys } + guard payload.devices.count == devices.count else { throw VaultPolicyError.invalidDeviceSet } + var revoked = 0 + try prefix { previous, next in + if previous == next { return true } + if sameDevice(previous, next), previous.status == .active, next.status == .revoked { + revoked += 1 + return true + } + return false + } + guard revoked == 1 else { throw VaultPolicyError.invalidDeviceSet } + case .rotateRecovery: + guard recoveryReplaced else { throw VaultPolicyError.invalidRecoveryKeys } + guard payload.devices == devices else { throw VaultPolicyError.invalidDeviceSet } + case .recoveryTransition: + guard recoveryUnchanged || recoveryReplaced else { throw VaultPolicyError.invalidRecoveryKeys } + try prefix { previous, next in + previous == next || (sameDevice(previous, next) && next.status == .revoked) + } + guard added.count == 1, added[0].status == .active else { throw VaultPolicyError.invalidDeviceSet } + } + } + + /// Binding for a content record written to `objectId` by `authorId` + /// under the current epoch and this membership. + func contentBinding(objectId: Data, authorId: Data) -> VaultRecordBinding { + VaultRecordBinding(kind: .content, vaultId: vaultId, generation: generation, epoch: epoch, + objectId: objectId, authorId: authorId, membershipHash: hash) + } + + func envelopeBinding(objectId: Data, epoch: UInt64, authorId: Data) -> VaultRecordBinding { + VaultRecordBinding(kind: .keyEnvelope, vaultId: vaultId, generation: generation, epoch: epoch, + objectId: objectId, authorId: authorId, membershipHash: hash) + } + + private static func payloadOf(_ encoded: Data) throws -> Data { + try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: VaultPolicy.maxPolicyBytes).untrustedPayload + } +} + +extension VaultRecordReader { + /// A bare byte string of exactly `count` bytes (array elements carry no key). + mutating func fixedBytes(_ count: Int) throws -> Data { + let length = try argument(major: 2) + guard length == UInt64(count) else { throw VaultRecordError.malformed } + return try takeBytes(count) + } +} diff --git a/apps/ios/Zeron/Sync/VaultRegistryCodec.swift b/apps/ios/Zeron/Sync/VaultRegistryCodec.swift new file mode 100644 index 000000000..d22a102a8 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultRegistryCodec.swift @@ -0,0 +1,128 @@ +import Foundation + +struct VaultRegistryCodec { + struct Field: Codable { + var kind: String + var id: String + var field: String + var hlc: String + var value: JSONValue + } + + /// Row lifecycle proof (purpose registryLifecycle, RFC 0001 §9): a + /// member's sealed statement that row `kind/id` was deleted at `hlc`. + struct Lifecycle: Codable { + var kind: String + var id: String + var op: String + var hlc: String + } + + let vault: MobileVault + let client: MobileVaultClient + let userId: String + private var object: Data { MobileVault.objectId(kind: "registry", id: userId) } + + func seal(_ batch: RegistryPendingBatch) async throws -> RegistryPendingBatch { + var sealed = batch + for index in sealed.ops.indices { + let op = sealed.ops[index] + if op.op == .delete { + let data = try JSONEncoder().encode(Lifecycle(kind: op.kind, id: op.id, op: "delete", hlc: op.hlc)) + let (bytes, _) = try await vault.seal(data, object: object, purpose: .registryLifecycle, maximum: 1024, client: client) + sealed.ops[index].proof = .object(["e1": .string(bytes.base64EncodedString())]) + continue + } + guard let fields = op.set else { continue } + var output: [String: JSONValue] = [:] + for (name, value) in fields { + let clock = op.clocks?[name] ?? op.hlc + let data = try JSONEncoder().encode(Field(kind: op.kind, id: op.id, field: name, hlc: clock, value: value)) + let (bytes, _) = try await vault.seal(data, object: object, purpose: .registryField, maximum: 8 * 1024, client: client) + output[name] = .object(["e1": .string(bytes.base64EncodedString())]) + } + sealed.ops[index].set = output + } + return sealed + } + + /// Open every field (and every tombstone proof). A tombstone without a + /// proof for exactly its row and clock is dropped — never applied — so a + /// relay cannot delete rows; a key that is not held yet still throws + /// (the caller withholds the batch and holds its cursor). + func open(_ rows: [RegistryRow]) async throws -> [RegistryRow] { + var output: [RegistryRow] = [] + for var row in rows { + if row.deleted { + guard let hlc = row.delHlc, let envelope = row.delProof?.objectValue, envelope.count == 1, + let text = envelope["e1"]?.stringValue, text.utf8.count <= 4096, + let bytes = Data(base64Encoded: text) else { continue } + let plaintext: Data + do { + plaintext = try await vault.open(bytes, object: object, purpose: .registryLifecycle, maximum: 1024, client: client) + } catch MobileVaultError.verification { + continue + } + guard let proof = try? JSONDecoder().decode(Lifecycle.self, from: plaintext), + proof.kind == row.kind, proof.id == row.id, proof.op == "delete", proof.hlc == hlc else { continue } + row.fields = [:] + row.clocks = [:] + output.append(row) + continue + } + var fields: [String: JSONValue] = [:] + var clocks: [String: String] = [:] + for (name, value) in row.fields { + guard let envelope = value.objectValue, envelope.count == 1, + let text = envelope["e1"]?.stringValue, text.utf8.count <= 24 * 1024, + let bytes = Data(base64Encoded: text), let clock = row.clocks[name] else { throw MobileVaultError.verification } + let plaintext = try await vault.open(bytes, object: object, purpose: .registryField, maximum: 8 * 1024, client: client) + let field = try JSONDecoder().decode(Field.self, from: plaintext) + guard field.kind == row.kind, field.id == row.id, field.field == name, field.hlc == clock else { throw MobileVaultError.verification } + fields[name] = field.value + clocks[name] = clock + } + row.fields = fields + row.clocks = clocks + output.append(row) + } + return output + } + + /// Merge opened rows against the verified baseline: fields by clock, + /// tombstones only when causally newer than the baseline, and a live + /// row over a verified tombstone only with a field newer than it + /// (RFC 0001 §9 — a relay can neither delete nor resurrect a row). + static func merge(_ rows: [RegistryRow], into baseline: [String: [String: RegistryRow]]) -> [RegistryRow] { + rows.compactMap { row in + let previous = baseline[row.kind]?[row.id] + if row.deleted { + if let previous { + let newer = previous.deleted ? hlcNewer(row.delHlc ?? "", previous.delHlc) + : hlcNewer(row.delHlc ?? "", maxClock(previous)) + guard newer else { return nil } + } + return row + } + var merged: RegistryRow + if let previous, !previous.deleted { + merged = previous + } else { + merged = RegistryRow(kind: row.kind, id: row.id, seq: row.seq, deleted: false, + delHlc: previous?.delHlc, fields: [:], clocks: [:]) + } + for (name, value) in row.fields { + guard let clock = row.clocks[name], hlcNewer(clock, merged.clocks[name]) || previous == nil else { continue } + if value.isNull { merged.fields.removeValue(forKey: name) } + else { merged.fields[name] = value } + merged.clocks[name] = clock + } + if let previous, previous.deleted, let gone = previous.delHlc, + !merged.clocks.values.contains(where: { $0 > gone }) { + return nil + } + merged.seq = row.seq + return merged + } + } +} diff --git a/apps/ios/Zeron/Sync/VaultSession.swift b/apps/ios/Zeron/Sync/VaultSession.swift new file mode 100644 index 000000000..1d7ae8883 --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultSession.swift @@ -0,0 +1,504 @@ +import CryptoKit +import Foundation + +enum MobileVaultError: LocalizedError { + case unavailable, notApproved, verification, storage, oversized, conflict + case http(Int) + + var errorDescription: String? { + switch self { + case .unavailable: return "Vault connection or keys are unavailable." + case .notApproved: return "Approve this device before using encrypted sync." + case .verification: return "Vault verification failed. Sync remains paused." + case .storage: return "Secure vault storage is unavailable. Existing data was retained." + case .oversized: return "Vault data exceeds the supported size." + case .conflict: return "The vault changed concurrently. The pending operation was retained." + case .http(let status): return "Vault request failed (HTTP \(status))." + } + } +} + +struct MobileVaultClient: Sendable { + let origin: URL + let orgId: String + let token: @Sendable () async -> String? + var transport: @Sendable (URLRequest) async throws -> (Data, Int) = { request in + let (stream, response) = try await URLSession.shared.bytes(for: request) + guard let response = response as? HTTPURLResponse else { throw MobileVaultError.unavailable } + if response.expectedContentLength > Int64(VaultPersistence.maxBytes) { throw MobileVaultError.oversized } + var data = Data() + for try await byte in stream { + guard data.count < VaultPersistence.maxBytes else { throw MobileVaultError.oversized } + data.append(byte) + } + return (data, response.statusCode) + } + + func request(_ path: String, method: String = "GET", body: Data? = nil, + contentType: String = "application/octet-stream") async throws -> (Data, Int) { + guard let bearer = await token() else { throw MobileVaultError.unavailable } + var components = URLComponents(url: origin.appending(path: "vault/\(orgId)"), resolvingAgainstBaseURL: false)! + let pieces = path.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false) + components.path += String(pieces[0]) + if pieces.count == 2 { components.percentEncodedQuery = String(pieces[1]) } + guard let url = components.url else { throw MobileVaultError.unavailable } + var request = URLRequest(url: url, timeoutInterval: 30) + request.httpMethod = method + request.httpBody = body + request.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization") + request.setValue(contentType, forHTTPHeaderField: "Content-Type") + let result = try await transport(request) + guard result.0.count <= VaultPersistence.maxBytes else { throw MobileVaultError.oversized } + return result + } +} + +enum MobileVaultPhase: String, Sendable { + case checking, legacy, notEnrolled, pending, ready, locked, keyUpdateRequired, verificationFailed, revoked, recoveryConfirmationRequired +} + +struct MobileVaultStatus: Sendable { + var phase: MobileVaultPhase + var fingerprint: String? + var membershipHash: String? + var deviceId: String? + var epoch: UInt64? + var pairingCode: String? + var message: String? +} + +actor MobileVault { + private struct Identity: Codable { + var id: Data + var signingSeed: Data + var agreementSeed: Data + var signingKey: Curve25519.Signing.PrivateKey { get throws { try .init(rawRepresentation: signingSeed) } } + var agreementKey: Curve25519.KeyAgreement.PrivateKey { get throws { try .init(rawRepresentation: agreementSeed) } } + var entry: VaultDeviceEntry { + get throws { try .init(deviceId: id, signingKey: signingKey.publicKey.rawRepresentation, + encryptionKey: agreementKey.publicKey.rawRepresentation, status: .active) } + } + } + private struct ObjectKey: Codable { + var object: Data + var epoch: UInt64 + var id: Data + var bytes: Data + } + private struct State: Codable { + var version = 1 + var required = true + var identity: Identity? + var fingerprint: Data? + var records: [Data] = [] + var keyring: Data? + var keys: [ObjectKey] = [] + var enrollment: Data? + var pendingMembership: Data? + var owed: [Data] = [] + var envelopes: [String: Data] = [:] + var recoverySecret: Data? + } + private struct MembershipPage: Decodable { + var records: [Data] + var truncated: Bool + var headSeq: Int64 + } + private struct ObjectKeys: Decodable { + struct Entry: Decodable { var record: Data } + var keys: [Entry] + } + private var state = State() + private var history: [VaultMembershipState] = [] + private var loaded = false + private var required = false + private var phase: MobileVaultPhase = .checking + private var message: String? + private var busy = false + private var waiters: [CheckedContinuation] = [] + private let persistence: VaultPersistence + private let profileHash: Data + + init(persistence: VaultPersistence, orgId: String, userId: String) { + self.persistence = persistence + profileHash = VaultPolicy.profileHash(orgId: orgId, userId: userId) + } + + func status() -> MobileVaultStatus { + var code: String? + if let identity = state.identity, let requestId = state.enrollment, + let head = history.last, let entry = try? identity.entry { + code = VaultEnrollmentRequest(vaultId: head.vaultId, requestId: requestId, deviceId: identity.id, + signingKey: entry.signingKey, encryptionKey: entry.encryptionKey) + .pairingCode(genesisHash: head.genesisHash) + } + return MobileVaultStatus(phase: phase, fingerprint: state.fingerprint?.vaultHex, membershipHash: history.last?.hash.vaultHex, + deviceId: state.identity?.id.vaultHex, epoch: history.last?.epoch, + pairingCode: code, message: message) + } + + private func acquire() async throws { + if busy { await withCheckedContinuation { waiters.append($0) } } else { busy = true } + if Task.isCancelled { release(); throw CancellationError() } + } + private func release() { + if waiters.isEmpty { busy = false } else { waiters.removeFirst().resume() } + } + + private func load() throws { + guard !loaded else { return } + do { + required = try persistence.exists || persistence.secrets.load(account: persistence.account) != nil + if let data = try persistence.load() { + let saved = try JSONDecoder().decode(State.self, from: data) + guard saved.version == 1 else { throw MobileVaultError.verification } + state = saved + required = true + try rebuild() + } else { + state = State() + history = [] + } + loaded = true + } catch { + phase = .locked + throw MobileVaultError.storage + } + } + + private func save() throws { + do { try persistence.save(JSONEncoder().encode(state)) } + catch { phase = .locked; loaded = false; throw MobileVaultError.storage } + required = true + } + + private func rebuild() throws { + history = try Self.verifyHistory(state.records, fingerprint: state.fingerprint, profileHash: profileHash) + if let identity = state.identity { + guard identity.id.count == 16, identity.signingSeed.count == 32, identity.agreementSeed.count == 32 else { throw MobileVaultError.verification } + _ = try identity.entry + } + if let ring = state.keyring { _ = try VaultKeyring.decode(ring) } + } + + static func verifyHistory(_ records: [Data], fingerprint: Data?, profileHash: Data) throws -> [VaultMembershipState] { + guard records.count <= 4096 else { throw MobileVaultError.oversized } + guard let first = records.first else { return [] } + guard let fingerprint, fingerprint.count == 32, VaultPolicy.membershipHash(first) == fingerprint else { throw MobileVaultError.verification } + let binding = try VaultUnverifiedRecord.parse(first, maxPayloadBytes: VaultPolicy.maxPolicyBytes).untrustedBinding + var history = [try VaultMembershipState.fromGenesis(first, expectedVaultId: binding.vaultId, + expectedGeneration: binding.generation, expectedProfileHash: profileHash)] + for record in records.dropFirst() { history.append(try history.last!.apply(record)) } + return history + } + + private func updatePhase() throws { + guard phase != .locked && phase != .verificationFailed else { return } + if state.pendingMembership != nil || !state.owed.isEmpty { phase = .keyUpdateRequired; return } + guard let head = history.last, let identity = state.identity else { + phase = required ? .notEnrolled : .legacy + return + } + guard let device = head.device(identity.id) else { phase = .pending; return } + guard device.status == .active else { phase = .revoked; return } + guard try device == identity.entry else { throw MobileVaultError.verification } + guard let encoded = state.keyring, try VaultKeyring.decode(encoded).epochKey(head.epoch) != nil else { + phase = .keyUpdateRequired + return + } + phase = state.recoverySecret == nil ? .ready : .recoveryConfirmationRequired + } + + private func fetchHistory(_ client: MobileVaultClient) async throws { + var records = state.records + while true { + let after = records.isEmpty ? "-1" : String(records.count - 1) + let (data, status) = try await client.request("/membership?after=\(after)") + guard status == 200 else { throw MobileVaultError.http(status) } + let page = try JSONDecoder().decode(MembershipPage.self, from: data) + guard page.records.count <= 256, records.count + page.records.count <= 4096 else { throw MobileVaultError.oversized } + records.append(contentsOf: page.records) + if !page.truncated { break } + guard !page.records.isEmpty else { throw MobileVaultError.verification } + } + let verified = try Self.verifyHistory(records, fingerprint: state.fingerprint, profileHash: profileHash) + if records != state.records { + state.records = records + try save() + } + history = verified + } + + private func envelopeContext(_ encoded: Data, object: Data) throws -> (VaultRecordBinding, Data) { + let binding = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: VaultEnvelope.maxPayloadBytes).untrustedBinding + guard binding.kind == .keyEnvelope, binding.objectId == object, + let revision = history.first(where: { $0.hash == binding.membershipHash }), + binding.epoch <= revision.epoch, let author = revision.activeDevice(binding.authorId) else { throw MobileVaultError.verification } + return (revision.envelopeBinding(objectId: object, epoch: binding.epoch, authorId: author.deviceId), author.signingKey) + } + + private func fetchKeyring(_ client: MobileVaultClient) async throws { + guard let identity = state.identity, let head = history.last, + head.activeDevice(identity.id) != nil else { return } + if let ring = state.keyring, try VaultKeyring.decode(ring).epochKey(head.epoch) != nil { return } + let (data, status) = try await client.request("/envelopes/\(identity.id.vaultHex)") + if status == 404 { return } + guard status == 200 else { throw MobileVaultError.http(status) } + let (binding, publicKey) = try envelopeContext(data, object: VaultPolicy.policyObjectId) + let incoming = try VaultEnvelope.openKeyring(data, expected: binding, recipientKind: .device, + recipientId: identity.id, recipientKey: identity.agreementKey, trustedPublicKey: publicKey) + var ring = try state.keyring.map(VaultKeyring.decode) ?? VaultKeyring() + try ring.merge(incoming) + state.keyring = ring.encode() + state.enrollment = nil + try save() + } + + func refresh(client: MobileVaultClient) async -> MobileVaultStatus { + do { + try await acquire() + defer { release() } + try load() + if phase == .locked { phase = .checking } + if required { try updatePhase() } + if state.pendingMembership != nil { try await finishMutation(client) } + let (_, status) = try await client.request("") + if status == 404 { + guard !required else { throw MobileVaultError.verification } + phase = .legacy + return self.status() + } + guard status == 200 else { throw MobileVaultError.http(status) } + required = true + try save() + if state.fingerprint != nil { + try await fetchHistory(client) + try await fetchKeyring(client) + if !state.owed.isEmpty { try await finishMutation(client) } + } + try updatePhase() + message = nil + } catch { + message = error is CancellationError ? nil : "Vault synchronization is paused. Retry when keys and connectivity are available." + if error is VaultPolicyError || error is VaultRecordError || error is VaultEnvelopeError { + phase = .verificationFailed + } else if let error = error as? MobileVaultError, case .verification = error { phase = .verificationFailed } + } + return status() + } + + struct ChannelCredentials: Sendable { + let deviceId: Data + let staticKey: Data + let membership: VaultMembershipState + } + + func channelCredentials(client: MobileVaultClient) async throws -> ChannelCredentials { + try await acquire() + defer { release() } + try load() + try await fetchHistory(client) + try updatePhase() + guard phase == .ready, let identity = state.identity, let head = history.last else { + throw MobileVaultError.notApproved + } + return ChannelCredentials(deviceId: identity.id, staticKey: identity.agreementSeed, membership: head) + } + + func acceptsChannel(peerId: Data, peerKey: Data) -> Bool { + guard phase == .ready, let head = history.last, let identity = state.identity, + head.activeDevice(identity.id) != nil else { return false } + return peerId != identity.id && head.activeDevice(peerId)?.encryptionKey == peerKey + } + + private func freshIdentity() throws -> Identity { + Identity(id: try VaultContentCrypto.randomBytes(16), signingSeed: Curve25519.Signing.PrivateKey().rawRepresentation, + agreementSeed: Curve25519.KeyAgreement.PrivateKey().rawRepresentation) + } + + func enroll(fingerprint: Data, client: MobileVaultClient) async throws { + try await acquire() + defer { release() } + try load() + guard fingerprint.count == 32, state.identity == nil || phase == .pending || phase == .notEnrolled || phase == .revoked else { throw MobileVaultError.notApproved } + if let pinned = state.fingerprint, pinned != fingerprint { throw MobileVaultError.verification } + if phase == .revoked { + // A removed identity stays revoked; reapproval admits a fresh one. + state.identity = try freshIdentity() + state.enrollment = nil + state.keyring = nil + } + phase = .pending + required = true + if let pinned = state.fingerprint, pinned != fingerprint { throw MobileVaultError.verification } + state.fingerprint = fingerprint + try await fetchHistory(client) + guard let head = history.last else { throw MobileVaultError.verification } + if state.identity == nil { state.identity = try freshIdentity() } + if state.enrollment == nil { state.enrollment = try VaultContentCrypto.randomBytes(16) } + try save() + let identity = state.identity! + let entry = try identity.entry + let enrollment = VaultEnrollmentRequest(vaultId: head.vaultId, requestId: state.enrollment!, deviceId: identity.id, + signingKey: entry.signingKey, encryptionKey: entry.encryptionKey) + let body: [String: String] = ["requestId": enrollment.requestId.vaultHex, "deviceId": identity.id.vaultHex, + "signingKey": entry.signingKey.vaultHex, "encryptionKey": entry.encryptionKey.vaultHex, + "proof": try identity.signingKey.signature(for: enrollment.proofInput).vaultHex] + let (_, status) = try await client.request("/enroll", method: "POST", body: JSONEncoder().encode(body), contentType: "application/json") + guard status == 200 || status == 201 else { throw MobileVaultError.http(status) } + } + + func recover(kit: String, fingerprint: Data, client: MobileVaultClient) async throws { + try await acquire() + defer { release() } + try load() + guard phase != .ready, state.pendingMembership == nil, fingerprint.count == 32 else { throw MobileVaultError.notApproved } + let recovery = try VaultRecoverySecret(kit: kit) + if let pinned = state.fingerprint, pinned != fingerprint { throw MobileVaultError.verification } + state.fingerprint = fingerprint + try await fetchHistory(client) + guard let head = history.last, try recovery.authorityId() == head.recoveryAuthorityId else { throw MobileVaultError.verification } + let (data, status) = try await client.request("/envelopes/\(head.recoveryAuthorityId.vaultHex)") + guard status == 200 else { throw MobileVaultError.http(status) } + let (binding, publicKey) = try envelopeContext(data, object: VaultPolicy.policyObjectId) + var ring = try VaultEnvelope.openKeyring(data, expected: binding, recipientKind: .recovery, recipientId: head.recoveryAuthorityId, + recipientKey: recovery.encryptionKey(), trustedPublicKey: publicKey) + guard head.epoch < UInt64.max, head.sequence < UInt64.max else { throw MobileVaultError.verification } + let identity = try freshIdentity() + var devices = head.devices + devices.append(try identity.entry) + let payload = VaultPolicyPayload(sequence: head.sequence + 1, parentHash: head.hash, profileHash: profileHash, + epoch: head.epoch + 1, operation: .recoveryTransition, recoverySigningKey: head.recoverySigningKey, + recoveryEncryptionKey: head.recoveryEncryptionKey, devices: devices) + let policyBinding = VaultPolicy.policyBinding(vaultId: head.vaultId, generation: head.generation, epoch: payload.epoch, + authorId: head.recoveryAuthorityId, parent: head.hash) + let record = try VaultEnvelope.sign(binding: policyBinding, revision: VaultContentCrypto.randomBytes(16), + payload: payload.encode(), signingKey: recovery.signingKey(), limit: VaultPolicy.maxPolicyBytes) + _ = try head.apply(record) + try ring.insert(epoch: payload.epoch, key: VaultContentCrypto.randomBytes(32)) + state.identity = identity + state.records.append(record) + state.keyring = ring.encode() + state.pendingMembership = record + state.enrollment = nil + state.owed = devices.filter { $0.status == .active }.map(\.deviceId) + [head.recoveryAuthorityId] + phase = .keyUpdateRequired + try save() + try rebuild() + try await finishMutation(client) + try updatePhase() + } + + private func finishMutation(_ client: MobileVaultClient) async throws { + if let record = state.pendingMembership { + guard let head = history.last, head.hash == VaultPolicy.membershipHash(record) else { throw MobileVaultError.verification } + let (_, status) = try await client.request("/membership", method: "POST", body: record) + if status != 200 { + let (data, code) = try await client.request("/membership?after=\(Int64(head.sequence) - 1)") + guard code == 200, try JSONDecoder().decode(MembershipPage.self, from: data).records.first == record else { throw MobileVaultError.conflict } + } + state.pendingMembership = nil + try save() + } + guard let head = history.last, let identity = state.identity, let ringBytes = state.keyring else { return } + let ring = try VaultKeyring.decode(ringBytes) + for recipient in state.owed { + let kind: VaultRecipientKind = recipient == head.recoveryAuthorityId ? .recovery : .device + let publicBytes = kind == .recovery ? head.recoveryEncryptionKey : head.activeDevice(recipient)?.encryptionKey + guard let publicBytes else { throw MobileVaultError.verification } + if state.envelopes[recipient.vaultHex] == nil { + state.envelopes[recipient.vaultHex] = try VaultEnvelope.sealKeyring( + binding: head.envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: head.epoch, authorId: identity.id), + kind: kind, recipientId: recipient, recipientKey: .init(rawRepresentation: publicBytes), + keyring: ring, signingKey: identity.signingKey) + try save() + } + let (_, status) = try await client.request("/envelopes/\(recipient.vaultHex)", method: "PUT", body: state.envelopes[recipient.vaultHex]) + guard status == 200 else { throw MobileVaultError.http(status) } + state.owed.removeAll { $0 == recipient } + state.envelopes.removeValue(forKey: recipient.vaultHex) + try save() + } + } + + static func encryptedRoomId(_ chatId: String) -> String { + if chatId.utf8.count <= 125 { return chatId + "-e1" } + return "e1-" + Data(SHA256.hash(data: Data("zeron/encrypted-room/v1\0\(chatId)".utf8)).prefix(20)).vaultHex + } + + static func objectId(kind: String, id: String) -> Data { + Data(SHA256.hash(data: Data("zeron/object-id/v1\0\(kind)\0\(id)".utf8)).prefix(16)) + } + + private func fetchObject(_ object: Data, client: MobileVaultClient) async throws { + guard let ringBytes = state.keyring else { throw MobileVaultError.notApproved } + let ring = try VaultKeyring.decode(ringBytes) + let (data, status) = try await client.request("/objects/\(object.vaultHex)/keys") + guard status == 200 else { throw MobileVaultError.http(status) } + let entries = try JSONDecoder().decode(ObjectKeys.self, from: data).keys + guard entries.count <= 1024 else { throw MobileVaultError.oversized } + for entry in entries { + let (binding, publicKey) = try envelopeContext(entry.record, object: object) + guard let epochKey = ring.epochKey(binding.epoch) else { continue } + let key = try VaultEnvelope.unwrapObjectKey(entry.record, expected: binding, epochKey: epochKey, trustedPublicKey: publicKey) + if let existing = state.keys.first(where: { $0.object == object && $0.epoch == binding.epoch }) { + guard existing.id == key.identifier, existing.bytes == key.exposeSecret() else { throw MobileVaultError.verification } + } else { + state.keys.append(ObjectKey(object: object, epoch: binding.epoch, id: key.identifier, bytes: key.exposeSecret())) + } + } + try save() + } + + func open(_ encoded: Data, object: Data, purpose: VaultContentPurpose, maximum: Int, + client: MobileVaultClient) async throws -> Data { + try await acquire() + defer { release() } + try load() + guard phase == .ready || phase == .revoked else { throw MobileVaultError.notApproved } + let untrusted = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: maximum + 144).untrustedBinding + if !history.contains(where: { $0.hash == untrusted.membershipHash }) { try await fetchHistory(client) } + guard let revision = history.first(where: { $0.hash == untrusted.membershipHash }), untrusted.epoch == revision.epoch, + untrusted.objectId == object, let author = revision.activeDevice(untrusted.authorId) else { throw MobileVaultError.verification } + if !state.keys.contains(where: { $0.object == object && $0.epoch == untrusted.epoch }) { try await fetchObject(object, client: client) } + guard let stored = state.keys.first(where: { $0.object == object && $0.epoch == untrusted.epoch }) else { throw MobileVaultError.unavailable } + let binding = revision.contentBinding(objectId: object, authorId: author.deviceId) + let key = try VaultContentKey(scope: VaultKeyScope(binding), identifier: stored.id, bytes: stored.bytes) + return try VaultContentCrypto.open(encoded, expected: binding, purpose: purpose, key: key, + trustedPublicKey: author.signingKey, maxPlaintextBytes: maximum).plaintext + } + + func prepareBatch(_ encoded: Data, object: Data, maximum: Int, client: MobileVaultClient) async throws -> (Data, String) { + let plaintext = try await open(encoded, object: object, purpose: .chatUpdate, maximum: maximum, client: client) + let record = try VaultUnverifiedRecord.parse(encoded, maxPayloadBytes: maximum + 144) + if phase == .ready, let head = history.last, let identity = state.identity, + record.untrustedBinding == head.contentBinding(objectId: object, authorId: identity.id) { + return (encoded, record.untrustedRevisionId.vaultHex) + } + return try await seal(plaintext, object: object, purpose: .chatUpdate, maximum: maximum, client: client) + } + + func seal(_ plaintext: Data, object: Data, purpose: VaultContentPurpose, maximum: Int, + client: MobileVaultClient) async throws -> (Data, String) { + try await acquire() + defer { release() } + guard phase == .ready, let head = history.last, let identity = state.identity, + head.activeDevice(identity.id) != nil else { throw MobileVaultError.notApproved } + if !state.keys.contains(where: { $0.object == object && $0.epoch == head.epoch }) { try await fetchObject(object, client: client) } + if !state.keys.contains(where: { $0.object == object && $0.epoch == head.epoch }) { + guard let bytes = state.keyring, let epochKey = try VaultKeyring.decode(bytes).epochKey(head.epoch) else { throw MobileVaultError.unavailable } + let binding = head.envelopeBinding(objectId: object, epoch: head.epoch, authorId: identity.id) + let key = try VaultContentKey(scope: VaultKeyScope(binding), identifier: VaultContentCrypto.randomBytes(16), bytes: VaultContentCrypto.randomBytes(32)) + let envelope = try VaultEnvelope.wrapObjectKey(binding: binding, epochKey: epochKey, key: key, signingKey: identity.signingKey) + let (_, status) = try await client.request("/objects/\(object.vaultHex)/keys", method: "PUT", body: envelope) + guard status == 200 || status == 409 else { throw MobileVaultError.http(status) } + try await fetchObject(object, client: client) + } + guard let stored = state.keys.first(where: { $0.object == object && $0.epoch == head.epoch }) else { throw MobileVaultError.unavailable } + let binding = head.contentBinding(objectId: object, authorId: identity.id) + let record = try VaultContentCrypto.seal(binding: binding, purpose: purpose, + key: VaultContentKey(scope: VaultKeyScope(binding), identifier: stored.id, bytes: stored.bytes), + signer: VaultDeviceSigner(authorId: identity.id, seed: identity.signingSeed), plaintext: plaintext, maxPlaintextBytes: maximum) + return (record.encoded, record.revisionId.vaultHex) + } +} diff --git a/apps/ios/Zeron/Sync/VaultSignedRecord.swift b/apps/ios/Zeron/Sync/VaultSignedRecord.swift new file mode 100644 index 000000000..f624bee0c --- /dev/null +++ b/apps/ios/Zeron/Sync/VaultSignedRecord.swift @@ -0,0 +1,258 @@ +import Foundation + +enum VaultRecordError: Error, Equatable { + case malformed + case nonCanonical + case unsupportedVersion + case unsupportedKind + case invalidEpoch + case sizeLimitExceeded + case contextMismatch + case invalidSignature +} + +enum VaultRecordKind: UInt64 { + case policy = 1 + case keyEnvelope = 2 + case content = 3 +} + +struct VaultRecordBinding: Equatable { + var kind: VaultRecordKind + var vaultId: Data + var generation: Data + var epoch: UInt64 + var objectId: Data + var authorId: Data + var membershipHash: Data +} + +struct VaultUnverifiedRecord: CustomStringConvertible, CustomDebugStringConvertible { + private let binding: VaultRecordBinding + private let revisionId: Data + private let payload: Data + private let signature: Data + private let maxPayloadBytes: Int + + var untrustedBinding: VaultRecordBinding { binding } + var untrustedRevisionId: Data { revisionId } + /// Payload bytes BEFORE verification — for decoding public routing + /// fields only (a policy record's device list must be read to find the + /// key that verifies it). Nothing decoded from here is trusted until + /// `verify` succeeds against an independently expected binding. + var untrustedPayload: Data { payload } + var description: String { "UnverifiedRecord([REDACTED])" } + var debugDescription: String { description } + + static func parse(_ encoded: Data, maxPayloadBytes: Int) throws -> Self { + guard encoded.count <= (try VaultRecordCodec.totalLimit(maxPayloadBytes)) else { + throw VaultRecordError.sizeLimitExceeded + } + var reader = VaultRecordReader(encoded) + guard try reader.argument(major: 5) == 11 else { throw VaultRecordError.malformed } + guard try reader.uintField(0) == 1 else { throw VaultRecordError.unsupportedVersion } + guard let kind = VaultRecordKind(rawValue: try reader.uintField(1)) else { + throw VaultRecordError.unsupportedKind + } + let vaultId = try reader.fixedField(2, count: 16) + let generation = try reader.fixedField(3, count: 16) + let epoch = try reader.uintField(4) + guard epoch > 0 else { throw VaultRecordError.invalidEpoch } + let objectId = try reader.fixedField(5, count: 16) + let authorId = try reader.fixedField(6, count: 16) + let revisionId = try reader.fixedField(7, count: 16) + let membershipHash = try reader.fixedField(8, count: 32) + let payload = try reader.bytesField(9, limit: maxPayloadBytes) + let signature = try reader.fixedField(10, count: 64) + guard reader.isAtEnd else { throw VaultRecordError.malformed } + return Self( + binding: VaultRecordBinding(kind: kind, vaultId: vaultId, generation: generation, epoch: epoch, + objectId: objectId, authorId: authorId, membershipHash: membershipHash), + revisionId: revisionId, payload: payload, signature: signature, maxPayloadBytes: maxPayloadBytes + ) + } + + func verify(expected: VaultRecordBinding, trustedPublicKey: Data) throws -> VaultVerifiedRecord { + guard binding == expected else { throw VaultRecordError.contextMismatch } + let input = try VaultRecordCodec.signingBytes( + binding: binding, revisionId: revisionId, payload: payload, maxPayloadBytes: maxPayloadBytes + ) + do { + try VaultCrypto.verifyEd25519(publicKey: trustedPublicKey, message: input, signature: signature) + } catch { + throw VaultRecordError.invalidSignature + } + return VaultVerifiedRecord(binding: binding, revisionId: revisionId, payload: payload) + } +} + +struct VaultVerifiedRecord: CustomStringConvertible, CustomDebugStringConvertible { + let binding: VaultRecordBinding + let revisionId: Data + let payload: Data + + fileprivate init(binding: VaultRecordBinding, revisionId: Data, payload: Data) { + self.binding = binding + self.revisionId = revisionId + self.payload = payload + } + + var description: String { "VerifiedRecord([REDACTED])" } + var debugDescription: String { description } +} + +enum VaultRecordCodec { + private static let domain = Data("zeron/signed-record/v1\0".utf8) + private static let maxOverhead = 256 + + static func signingBytes( + binding: VaultRecordBinding, revisionId: Data, payload: Data, maxPayloadBytes: Int + ) throws -> Data { + var out = try buffer(binding: binding, revisionId: revisionId, payload: payload, limit: maxPayloadBytes) + out.append(domain) + fields(into: &out, count: 10, binding: binding, revisionId: revisionId, payload: payload) + return out + } + + static func encodeSigned( + binding: VaultRecordBinding, revisionId: Data, payload: Data, signature: Data, maxPayloadBytes: Int + ) throws -> Data { + guard signature.count == 64 else { throw VaultRecordError.malformed } + var out = try buffer(binding: binding, revisionId: revisionId, payload: payload, limit: maxPayloadBytes) + fields(into: &out, count: 11, binding: binding, revisionId: revisionId, payload: payload) + bytesField(into: &out, key: 10, value: signature) + return out + } + + fileprivate static func totalLimit(_ payloadLimit: Int) throws -> Int { + guard payloadLimit >= 0, payloadLimit <= Int.max - maxOverhead else { throw VaultRecordError.sizeLimitExceeded } + return payloadLimit + maxOverhead + } + + private static func buffer(binding: VaultRecordBinding, revisionId: Data, payload: Data, limit: Int) throws -> Data { + _ = try totalLimit(limit) + guard binding.epoch > 0 else { throw VaultRecordError.invalidEpoch } + guard payload.count <= limit else { throw VaultRecordError.sizeLimitExceeded } + guard [binding.vaultId, binding.generation, binding.objectId, binding.authorId, revisionId].allSatisfy({ $0.count == 16 }), + binding.membershipHash.count == 32 else { throw VaultRecordError.malformed } + return Data(capacity: payload.count + maxOverhead) + } + + static func contextBytes(binding: VaultRecordBinding, revisionId: Data) throws -> Data { + var output = try buffer(binding: binding, revisionId: revisionId, payload: Data(), limit: 0) + headerFields(into: &output, count: 9, binding: binding, revisionId: revisionId) + return output + } + + private static func fields(into out: inout Data, count: UInt64, binding: VaultRecordBinding, revisionId: Data, payload: Data) { + headerFields(into: &out, count: count, binding: binding, revisionId: revisionId) + bytesField(into: &out, key: 9, value: payload) + } + + private static func headerFields(into out: inout Data, count: UInt64, binding: VaultRecordBinding, revisionId: Data) { + argument(into: &out, major: 5, value: count) + uintField(into: &out, key: 0, value: 1) + uintField(into: &out, key: 1, value: binding.kind.rawValue) + bytesField(into: &out, key: 2, value: binding.vaultId) + bytesField(into: &out, key: 3, value: binding.generation) + uintField(into: &out, key: 4, value: binding.epoch) + bytesField(into: &out, key: 5, value: binding.objectId) + bytesField(into: &out, key: 6, value: binding.authorId) + bytesField(into: &out, key: 7, value: revisionId) + bytesField(into: &out, key: 8, value: binding.membershipHash) + } + + static func uintField(into out: inout Data, key: UInt64, value: UInt64) { + argument(into: &out, major: 0, value: key) + argument(into: &out, major: 0, value: value) + } + + static func bytesField(into out: inout Data, key: UInt64, value: Data) { + argument(into: &out, major: 0, value: key) + argument(into: &out, major: 2, value: UInt64(value.count)) + out.append(value) + } + + static func argument(into out: inout Data, major: UInt8, value: UInt64) { + if value < 24 { + out.append((major << 5) | UInt8(value)) + return + } + let width: Int + let additional: UInt8 + switch value { + case 24...0xff: (width, additional) = (1, 24) + case 0x100...0xffff: (width, additional) = (2, 25) + case 0x10000...0xffffffff: (width, additional) = (4, 26) + default: (width, additional) = (8, 27) + } + out.append((major << 5) | additional) + var bigEndian = value.bigEndian + withUnsafeBytes(of: &bigEndian) { out.append(contentsOf: $0.suffix(width)) } + } +} + +struct VaultRecordReader { + private let data: Data + private var offset: Int + + init(_ data: Data) { + self.data = data + self.offset = data.startIndex + } + + var isAtEnd: Bool { offset == data.endIndex } + + private mutating func take(_ count: Int) throws -> Data { + guard count >= 0, count <= data.endIndex - offset else { throw VaultRecordError.malformed } + let start = offset + offset += count + return data[start.. Data { + try take(count) + } + + mutating func argument(major: UInt8) throws -> UInt64 { + let first = try take(1) + let head = first[first.startIndex] + guard head >> 5 == major else { throw VaultRecordError.malformed } + let length: Int + let minimum: UInt64 + switch head & 31 { + case 0...23: return UInt64(head & 31) + case 24: (length, minimum) = (1, 24) + case 25: (length, minimum) = (2, 0x100) + case 26: (length, minimum) = (4, 0x10000) + case 27: (length, minimum) = (8, 0x100000000) + default: throw VaultRecordError.malformed + } + let value = try take(length).reduce(UInt64(0)) { ($0 << 8) | UInt64($1) } + guard value >= minimum else { throw VaultRecordError.nonCanonical } + return value + } + + private mutating func key(_ expected: UInt64) throws { + guard try argument(major: 0) == expected else { throw VaultRecordError.malformed } + } + + mutating func uintField(_ key: UInt64) throws -> UInt64 { + try self.key(key) + return try argument(major: 0) + } + + mutating func bytesField(_ key: UInt64, limit: Int) throws -> Data { + try self.key(key) + let length = try argument(major: 2) + guard length <= UInt64(limit) else { throw VaultRecordError.sizeLimitExceeded } + return try take(Int(length)) + } + + mutating func fixedField(_ key: UInt64, count: Int) throws -> Data { + let value = try bytesField(key, limit: count) + guard value.count == count else { throw VaultRecordError.malformed } + return value + } +} diff --git a/apps/ios/Zeron/Sync/WorkspaceStore.swift b/apps/ios/Zeron/Sync/WorkspaceStore.swift index 4eada43a1..4325b409d 100644 --- a/apps/ios/Zeron/Sync/WorkspaceStore.swift +++ b/apps/ios/Zeron/Sync/WorkspaceStore.swift @@ -2,9 +2,9 @@ // host (crates/doc/src/registry.rs + crates/engine WorkspaceHost). Joins the // per-user `/registry/{orgId}/ws` room, projects the row table into typed // rows, and performs the writes the writer discipline allows a viewer device: -// chat creates, archives, renames and seen marks. iOS is a viewport, not an -// engine device, so it owns no device row; it does publish a presence beat -// (registry presence replaced the old ws room's ephemeral store). +// chat creates, archives, renames and seen marks. An enrolled phone also +// publishes its own device row so approved devices have recognizable names. +// Liveness is published separately through registry presence beats. // // Reads are OVERLAY reads: the server's authoritative rows plus the pending // op-batch queue replayed on top (optimistic local writes, retired on ack). @@ -49,9 +49,27 @@ final class WorkspaceStore { /// restarts on every rejoin. @ObservationIgnored private var registryJoinedAt: Int64? private let config: AppConfig + let encrypted: Bool + private(set) var syncError: String? + @ObservationIgnored private var stopped = false + @ObservationIgnored private var sealedBatches: [String: RegistryPendingBatch] = [:] + @ObservationIgnored private let http = URLSession(configuration: .default) + private struct StoredRegistry: Codable { + var version = 1 + var doc: Data + var batches: [String: RegistryPendingBatch] + } + private var registryURL: URL { + encrypted ? config.vaultPersistence.directory.appendingPathComponent("registry.snapshot") + : DocDisk.registryURL(orgId: config.orgId, userId: config.userId) + } + private var codec: VaultRegistryCodec { + VaultRegistryCodec(vault: config.vault, client: config.vaultClient, userId: config.userId) + } init(config: AppConfig) { self.config = config + encrypted = config.syncAccess == .encrypted self.doc = RegistryDoc(deviceId: config.deviceId) } @@ -61,24 +79,35 @@ final class WorkspaceStore { // sidebar renders immediately and the hello backfills from our // cursor. First run after the update: no blob → cursor null → the // server's full state (the engines already seeded everything). - let blobURL = DocDisk.registryURL(orgId: config.orgId, userId: config.userId) - if let data = try? Data(contentsOf: blobURL), - let loaded = try? RegistryDoc.from(data: data, deviceId: config.deviceId) { + let blobURL = registryURL + if encrypted { + do { + if FileManager.default.fileExists(atPath: blobURL.path) { + let size = try blobURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? Int.max + guard size <= VaultChatDisk.maxBytes else { throw VaultStorageError.tooLarge } + let saved = try JSONDecoder().decode(StoredRegistry.self, from: Data(contentsOf: blobURL)) + guard saved.version == 1 else { throw VaultStorageError.invalidState } + doc = try RegistryDoc.from(data: saved.doc, deviceId: config.deviceId) + sealedBatches = saved.batches + } + } catch { syncError = "Encrypted registry storage could not be opened."; return } + } else if let data = try? Data(contentsOf: blobURL), + let loaded = try? RegistryDoc.from(data: data, deviceId: config.deviceId) { doc = loaded } project() saver = RegistrySaver(url: blobURL) { [weak self] in - try? self?.doc.toData() + try? self?.snapshotBytes() } let delegate = RegistryClient.Delegate( helloCursor: { [weak self] in self?.doc.helloCursor ?? nil }, - takePushable: { [weak self] in self?.doc.takePushable() ?? [] }, - event: { [weak self] event in self?.handle(event) } + takePushable: { [weak self] in await self?.takePushable() ?? [] }, + event: { [weak self] event in await self?.handle(event) } ) let client = RegistryClient(device: config.deviceId, - urlProvider: { [config] in await config.registrySocketURL() }, - delegate: delegate) + urlProvider: { [config, encrypted] in await config.registrySocketURL(encrypted: encrypted) }, + delegate: delegate, sendAllowed: { [config, encrypted] in config.permitsSync(encrypted: encrypted) }) self.client = client Task { await client.start() } // Pull-first bootstrap + poll-while-down: one HTTPS GET syncs the @@ -104,8 +133,134 @@ final class WorkspaceStore { /// The WS hello's delta answer over plain HTTPS, applied through the /// exact state path the socket uses. + private func snapshotBytes() throws -> Data { + let data = try doc.toData() + return encrypted ? try JSONEncoder().encode(StoredRegistry(doc: data, batches: sealedBatches)) : data + } + + private func persistRegistry() throws { + let bytes = try snapshotBytes() + guard bytes.count <= VaultChatDisk.maxBytes else { throw VaultStorageError.tooLarge } + try FileManager.default.createDirectory(at: registryURL.deletingLastPathComponent(), withIntermediateDirectories: true) + try VaultPersistence.writeDurably(bytes, to: registryURL) + } + + /// Reconcile display metadata after every vault refresh. Reapproval gives + /// this installation a new identity; retain the previous identity's label. + func publishDeviceIdentity(vaultId: String) { + guard encrypted, !stopped, config.permitsSync(encrypted: true) else { return } + let row = doc.overlayRow(kind: "devices", id: config.deviceId) + let name = doc.overlayRow(kind: "vaultDevices", id: vaultId)?.fields["name"]?.stringValue + ?? row?.fields["name"]?.stringValue ?? config.deviceName + let previous = row?.fields["vaultDeviceId"]?.stringValue + var changed = false + for id in Set([previous, vaultId].compactMap { $0 }) { + if doc.overlayRow(kind: "vaultDevices", id: id) == nil { + doc.write(kind: "vaultDevices", id: id, op: .upsert, set: [ + "name": .string(name), "deviceId": .string(config.deviceId), "platform": .string("ios") + ]) + changed = true + } + } + if let alias = doc.overlayRow(kind: "vaultDevices", id: vaultId), + alias.fields["deviceId"]?.stringValue != config.deviceId { + doc.write(kind: "vaultDevices", id: vaultId, op: .update, set: [ + "deviceId": .string(config.deviceId), "platform": .string("ios") + ]) + changed = true + } + if previous != vaultId || row?.fields["name"]?.stringValue != name { + doc.write(kind: "devices", id: config.deviceId, op: .upsert, set: [ + "id": .string(config.deviceId), "name": .string(name), + "platform": .string("ios"), "vaultDeviceId": .string(vaultId) + ]) + changed = true + } + if changed { afterLocalWrite() } + } + + @ObservationIgnored private var sealingBatches = false + private func takePushable() async -> [RegistryPendingBatch] { + guard !stopped, !sealingBatches, config.permitsSync(encrypted: encrypted) else { return [] } + let batches = doc.takePushable() + guard encrypted else { return batches } + sealingBatches = true + defer { sealingBatches = false } + var result: [RegistryPendingBatch] = [] + do { + for batch in batches { + let status = await config.vault.status() + func current(_ candidate: RegistryPendingBatch) -> Bool { + candidate.ops.allSatisfy { op in + (op.set ?? [:]).values.allSatisfy { value in + guard let text = value.objectValue?["e1"]?.stringValue, + let bytes = Data(base64Encoded: text), + let record = try? VaultUnverifiedRecord.parse(bytes, maxPayloadBytes: 8 * 1024 + 144) else { return false } + return record.untrustedBinding.membershipHash.vaultHex == status.membershipHash + } + } + } + if sealedBatches[batch.batch].map(current) != true { + sealedBatches[batch.batch] = try await codec.seal(batch) + } + guard !stopped, !Task.isCancelled, config.permitsSync(encrypted: true), + let sealed = sealedBatches[batch.batch], current(sealed) else { throw MobileVaultError.unavailable } + result.append(sealed) + } + try persistRegistry() + return result + } catch { + doc.markDisconnected() + syncError = "Registry changes remain queued until they can be encrypted and saved." + return [] + } + } + + private func handle(_ event: RegistryEvent) async { + guard !stopped else { return } + guard encrypted else { handleVerified(event); return } + var previous: Data? + var oldBatches = sealedBatches + func checkpoint() throws { + previous = try doc.toData() + oldBatches = sealedBatches + } + do { + switch event { + case .state(let seq, _, let floor, let rows, let beats): + let opened = try await codec.open(rows) + guard !stopped, config.permitsSync(encrypted: true) else { return } + try checkpoint() + handleVerified(.state(seq: seq, full: false, gcFloor: floor, + rows: VaultRegistryCodec.merge(opened, into: doc.authoritative), presence: beats)) + case .rows(let seq, let rows): + let opened = try await codec.open(rows) + guard !stopped, config.permitsSync(encrypted: true) else { return } + try checkpoint() + handleVerified(.rows(seq: seq, rows: VaultRegistryCodec.merge(opened, into: doc.authoritative))) + case .ack(let batch, _, _): + try checkpoint() + handleVerified(event) + sealedBatches.removeValue(forKey: batch) + default: + try checkpoint() + handleVerified(event) + } + try persistRegistry() + syncError = nil + } catch { + if let previous, let restored = try? RegistryDoc.from(data: previous, deviceId: config.deviceId) { + doc = restored + sealedBatches = oldBatches + } + syncError = "Registry sync paused: data could not be verified or saved." + connected = false + project() + } + } + private func pullDelta() async { - guard let request = await config.registryRowsRequest(since: doc.helloCursor) else { + guard let request = await config.registryRowsRequest(since: doc.helloCursor, encrypted: encrypted) else { roomLog.warning("registry: http pull skipped — no URL (token unavailable)") return } @@ -129,7 +284,7 @@ final class WorkspaceStore { roomLog.warning("registry: http pull body unparseable") return } - handle(.state(seq: body.seq, full: body.full, gcFloor: body.gcFloor ?? 0, + await handle(.state(seq: body.seq, full: body.full, gcFloor: body.gcFloor ?? 0, rows: body.rows, presence: body.presence ?? [:])) } @@ -145,10 +300,10 @@ final class WorkspaceStore { var seq: UInt64 var applied: UInt64? } - let batches = doc.takePushable() + let batches = await takePushable() guard !batches.isEmpty else { return } for pending in batches { - guard var request = await config.registryPushRequest(), + guard var request = await config.registryPushRequest(encrypted: encrypted), let body = try? JSONEncoder().encode(PushBody(batch: pending.batch, ops: pending.ops)) else { break } request.httpBody = body @@ -163,7 +318,7 @@ final class WorkspaceStore { doc.markDisconnected() break } - handle(.ack(batch: ack.batch, seq: ack.seq, applied: ack.applied ?? 0)) + await handle(.ack(batch: ack.batch, seq: ack.seq, applied: ack.applied ?? 0)) } } @@ -200,7 +355,7 @@ final class WorkspaceStore { // MARK: Server events (delivered in frame order — rows before ack) - private func handle(_ event: RegistryEvent) { + private func handleVerified(_ event: RegistryEvent) { switch event { case .state(let seq, let full, let gcFloor, let rows, let beats): // On a state frame with full=true and seq < our cursor (server diff --git a/apps/ios/Zeron/Transcript/ToolBlobView.swift b/apps/ios/Zeron/Transcript/ToolBlobView.swift new file mode 100644 index 000000000..2c0d46920 --- /dev/null +++ b/apps/ios/Zeron/Transcript/ToolBlobView.swift @@ -0,0 +1,73 @@ +import SwiftUI + +private struct ToolBlobLoaderKey: EnvironmentKey { + static let defaultValue: (@MainActor (String) async throws -> String)? = nil +} +extension EnvironmentValues { + var toolBlobLoader: (@MainActor (String) async throws -> String)? { + get { self[ToolBlobLoaderKey.self] } + set { self[ToolBlobLoaderKey.self] = newValue } + } +} +struct ToolBlobSelection: Identifiable { + let id: String + let title: String +} +struct ToolBlobView: View { + let selection: ToolBlobSelection + let load: @MainActor (String) async throws -> String + @Environment(\.dismiss) private var dismiss + @State private var output: String? + @State private var error: String? + @State private var attempt = 0 + var body: some View { + NavigationStack { + Group { + if let output { + ScrollView([.vertical, .horizontal]) { + Text(output).font(Theme.mono(12)).textSelection(.enabled).padding() + } + } else if let error { + ContentUnavailableView { + Label("Output unavailable", systemImage: "exclamationmark.triangle") + } description: { Text(error) } actions: { + Button("Retry") { attempt += 1 } + } + } else { ProgressView("Loading…") } + } + .navigationTitle(selection.title.replacingOccurrences(of: "Show full ", with: "").capitalized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } + if let output { + ToolbarItem(placement: .topBarLeading) { + Button("Copy", systemImage: "doc.on.doc") { UIPasteboard.general.string = output } + } + } + } + .task(id: attempt) { + error = nil + do { + let text = try await load(selection.id) + if !Task.isCancelled { + if selection.id.hasSuffix(".diff") { + output = try Self.readableDiff(text) + } else { output = text } + } + } catch { + if !Task.isCancelled { self.error = error.localizedDescription } + } + } + } + } + + private static func readableDiff(_ text: String) throws -> String { + struct Diff: Decodable { let path: String; let oldText: String?; let newText: String } + let diff = try JSONDecoder().decode(Diff.self, from: Data(text.utf8)) + if let old = diff.oldText { + return "\(diff.path)\n\nBefore\n\(old)\n\nAfter\n\(diff.newText)" + } + return "\(diff.path) (new file)\n\n\(diff.newText)" + } + +} diff --git a/apps/ios/Zeron/Transcript/TranscriptView.swift b/apps/ios/Zeron/Transcript/TranscriptView.swift index 2dc4cd735..e52543fac 100644 --- a/apps/ios/Zeron/Transcript/TranscriptView.swift +++ b/apps/ios/Zeron/Transcript/TranscriptView.swift @@ -32,7 +32,8 @@ struct TranscriptView: View { isTail: row.id == rows.last?.id || (row.entryId == runway && row.turnStart), chatId: chatId)) .environment(\.dynamicTypeSize, dynamicTypeSize) - .environment(\.colorScheme, .dark)) + .environment(\.colorScheme, .dark) + .environment(\.toolBlobLoader, { ref in try await store.fetchToolBlob(ref: ref) })) } .modifier(TranscriptViewportProbe(chatId: chatId)) .background(Theme.bg) @@ -499,9 +500,12 @@ struct ToolChipRow: View { var continues = false var onResize: () -> Void = {} @State private var expanded = false + @State private var selectedBlob: ToolBlobSelection? + @Environment(\.toolBlobLoader) private var loadBlob @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { + VStack(alignment: .leading, spacing: 4) { Button { withAnimation(reduceMotion ? nil : Motion.resize) { expanded.toggle() } onResize() @@ -552,6 +556,29 @@ struct ToolChipRow: View { UIPasteboard.general.string = tool.call.expandedDetail } } + if expanded { + if let output = tool.call.string("output"), !output.isEmpty { + Text(output).font(Theme.mono(12)).foregroundStyle(Theme.textMuted) + .textSelection(.enabled).padding(.leading, 36) + } + if loadBlob != nil { + ForEach(blobSelections) { selection in + Button(selection.title) { selectedBlob = selection } + .font(Theme.sans(13)).padding(.leading, 36).frame(minHeight: 44) + } + } + } + } + .sheet(item: $selectedBlob) { selection in + if let loadBlob { ToolBlobView(selection: selection, load: loadBlob) } + } + .onChange(of: expanded) { _, _ in onResize() } + } + + private var blobSelections: [ToolBlobSelection] { + [("outputRef", "Show full output"), ("diffRef", "Show full diff")].compactMap { key, title in + tool.call.string(key).map { ToolBlobSelection(id: $0, title: title) } + } } } diff --git a/apps/ios/Zeron/Views/EncryptionView.swift b/apps/ios/Zeron/Views/EncryptionView.swift new file mode 100644 index 000000000..b45f07b0e --- /dev/null +++ b/apps/ios/Zeron/Views/EncryptionView.swift @@ -0,0 +1,321 @@ +import SwiftUI + +/// Encrypted sync on the phone (RFC 0001 §4.2): the vault state, the +/// comparison code while an approval is pending, and the two ways in — +/// approval from an already-approved device, or the recovery kit. The +/// vault fingerprint typed here comes from the approving device (Settings → +/// Encryption → "Copy vault fingerprint", or `zeron vault status`) and pins +/// the genesis so a relay cannot hand this phone a substitute vault. +struct EncryptionView: View { + var requiresApproval = false + @Environment(AppModel.self) private var model + @Environment(\.dismiss) private var dismiss + @State private var fingerprint = "" + @State private var kit = "" + @State private var error: String? + @State private var working = false + + private var status: MobileVaultStatus? { model.vaultStatus } + + var body: some View { + NavigationStack { + List { + Section { + LabeledContent("Status", value: statusTitle) + Text(statusCopy) + .font(.footnote) + .foregroundStyle(.secondary) + if let fingerprint = status?.fingerprint { + LabeledContent("Vault fingerprint") { + Text(fingerprint.prefix(16) + "…") + .font(.system(.footnote, design: .monospaced)) + .textSelection(.enabled) + } + } + if let epoch = status?.epoch { + LabeledContent("Key epoch", value: String(epoch)) + } + } header: { + Text("End-to-end encryption") + } + + if let code = status?.pairingCode, status?.phase == .pending { + Section { + Text(code) + .font(.system(size: 34, weight: .semibold, design: .monospaced)) + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + Text("On the approving device, approve only if it shows exactly this code.") + .font(.footnote) + .foregroundStyle(.secondary) + } header: { + Text("Comparison code") + } + } + + if canEnroll { + Section { + TextField("Vault fingerprint (64 hex characters)", text: $fingerprint) + .font(.system(.footnote, design: .monospaced)) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + Button("Approve from another device") { run { try await model.enrollVault(fingerprintHex: fingerprint) } } + .disabled(working || fingerprint.count < 64) + } header: { + Text("Approve this device") + } footer: { + Text("Paste the vault fingerprint shown on an approved device. That device then compares the code above before approving. An approved device can read all synced content and manage devices.") + } + + Section { + TextField("Recovery key (XXXXX-XXXXX-…)", text: $kit) + .font(.system(.footnote, design: .monospaced)) + .textInputAutocapitalization(.characters) + .autocorrectionDisabled() + Button("Use recovery key") { run { try await model.recoverVault(kit: kit, fingerprintHex: fingerprint) } } + .disabled(working || kit.count < 55 || fingerprint.count < 64) + } header: { + Text("Recovery") + } footer: { + Text("Enter the recovery key and the vault fingerprint from your recovery file. This adds the phone under a fresh key epoch; other devices catch up automatically.") + } + } + + if let error { + Section { + Text(error).foregroundStyle(.red) + } + } + } + .navigationTitle("Encryption") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if requiresApproval { + Button("Sign out") { model.signOut() } + } else { + Button("Done") { dismiss() } + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + model.refreshVault() + } label: { + if model.vaultBusy || working { + ProgressView() + } else { + Image(systemName: "arrow.clockwise") + } + } + .disabled(model.vaultBusy || working) + } + } + } + .onAppear { + if fingerprint.isEmpty, let known = status?.fingerprint { fingerprint = known } + model.refreshVault() + } + .task(id: status?.phase) { + guard status?.phase == .pending || status?.phase == .keyUpdateRequired else { return } + while !Task.isCancelled { + do { try await Task.sleep(for: .seconds(2)) } catch { return } + model.refreshVault() + } + } + } + + private var canEnroll: Bool { + switch status?.phase { + case .notEnrolled, .revoked, .legacy, .pending, nil: return true + default: return false + } + } + + private var statusTitle: String { + switch status?.phase { + case .ready: return "Encrypted" + case .legacy: return "Not set up" + case .notEnrolled: return "Approve this device" + case .pending: return "Waiting for approval" + case .locked: return "Locked" + case .keyUpdateRequired: return "Waiting for keys" + case .verificationFailed: return "Sync paused" + case .revoked: return "This device was removed" + case .recoveryConfirmationRequired: return "Confirm recovery kit" + case .checking, nil: return "Checking…" + } + } + + private var statusCopy: String { + if let message = status?.message { return message } + switch status?.phase { + case .ready: + return "Synced content is encrypted on your devices. Only approved devices, or someone with your recovery key, can read it." + case .legacy: + return "This account has no encrypted vault. Set one up on a desktop (Settings → Encryption or `zeron vault setup`), then approve this phone." + case .revoked: + return "This phone no longer has access to encrypted sync. Ask an approved device to approve it again, or use your recovery key." + case .notEnrolled: + return "This account uses end-to-end encryption. Nothing syncs to this phone until an approved device admits it." + case .pending: + return "Open Settings → Encryption on an approved device and compare the code." + case .locked: + return "Secure key storage is unavailable on this phone. Existing data was retained." + case .keyUpdateRequired: + return "Another device changed the vault's keys; sync resumes once the new keys arrive." + case .verificationFailed: + return "Data from the sync backend could not be verified. Sync stays paused." + case .recoveryConfirmationRequired: + return "Confirm the recovery kit on the device that created the vault." + case .checking, nil: + return "Checking the vault for this account." + } + } + + private func run(_ operation: @escaping () async throws -> Void) { + error = nil + working = true + Task { + do { try await operation() } catch { self.error = error.localizedDescription } + working = false + } + } +} + +/// The everyday approval flow. Protocol details stay in the optional settings sheet. +struct DeviceApprovalView: View { + @Environment(AppModel.self) private var model + @State private var fingerprint = "" + @State private var recoveryKey = "" + @State private var working = false + @State private var error: String? + @State private var showRecovery = false + @State private var showDetails = false + + private var pending: Bool { model.vaultStatus?.phase == .pending } + private var finishing: Bool { model.vaultStatus?.phase == .keyUpdateRequired } + private var knownFingerprint: String { model.vaultStatus?.fingerprint ?? fingerprint.trimmingCharacters(in: .whitespacesAndNewlines) } + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 28) { + Image(systemName: pending ? "laptopcomputer" : "iphone.and.arrow.forward") + .font(.system(size: 44, weight: .light)) + .foregroundStyle(Theme.accent) + .frame(width: 108, height: 108) + .background(Theme.accent.opacity(0.12), in: RoundedRectangle(cornerRadius: 30)) + .accessibilityHidden(true) + + VStack(spacing: 12) { + Text(finishing ? "Getting your chats ready" : pending ? "Check your desktop" : "Get approval to\nview your chats") + .font(.system(.largeTitle, design: .rounded, weight: .semibold)) + .multilineTextAlignment(.center) + Text(finishing ? "Your device has been approved. This will only take a moment." : pending ? "Open the approval request on your desktop and make sure these numbers match." : "Your chats are private. Use a device you’ve already approved to let this iPhone access them.") + .font(.body) + .foregroundStyle(Theme.textMuted) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + + if pending, let code = model.vaultStatus?.pairingCode { + VStack(spacing: 16) { + Text("MATCH THESE NUMBERS") + .font(.caption.weight(.medium)).tracking(1.5) + .foregroundStyle(Theme.textMuted) + Text(String(code.prefix(4)) + " " + String(code.dropFirst(4))) + .font(.system(size: 36, weight: .medium, design: .monospaced)) + .minimumScaleFactor(0.7).lineLimit(1) + .accessibilityLabel("Comparison code") + .accessibilityValue(code.map(String.init).joined(separator: " ")) + HStack(spacing: 8) { + ProgressView().controlSize(.small) + Text("Waiting for approval…").font(.footnote) + }.foregroundStyle(Theme.textMuted) + } + .frame(maxWidth: .infinity).padding(24) + .background(Theme.surfaceRaised, in: RoundedRectangle(cornerRadius: 24)) + } else if finishing { + ProgressView() + } else { + if model.vaultStatus?.fingerprint == nil { + VStack(alignment: .leading, spacing: 10) { + Text("Connect to your desktop").font(.headline) + Text("On your desktop, open Settings → Encryption and copy the vault fingerprint. Paste it here once to connect securely.") + .font(.footnote).foregroundStyle(Theme.textMuted) + TextField("Paste connection fingerprint", text: $fingerprint, axis: .vertical) + .textInputAutocapitalization(.never).autocorrectionDisabled() + .font(.system(.footnote, design: .monospaced)) + .padding(12).background(Theme.surfaceRaised, in: RoundedRectangle(cornerRadius: 12)) + } + } + Button { run { try await model.enrollVault(fingerprintHex: knownFingerprint) } } label: { + HStack(spacing: 8) { + if working { ProgressView().tint(.black) } + Text("Ask for approval").font(.headline) + }.frame(maxWidth: .infinity).padding(.vertical, 17) + } + .buttonStyle(.plain).foregroundStyle(.black) + .background(.white, in: Capsule()) + .disabled(working || knownFingerprint.count != 64) + .opacity(working || knownFingerprint.count != 64 ? 0.5 : 1) + } + if let error { + Text(error).font(.footnote).foregroundStyle(Theme.danger) + .multilineTextAlignment(.center) + } + Button("Use a recovery key instead") { showRecovery = true } + .font(.subheadline).foregroundStyle(Theme.textMuted) + .disabled(working) + Label("Your chats stay end-to-end encrypted", systemImage: "lock.fill") + .font(.caption).foregroundStyle(Theme.textFaint) + } + .frame(maxWidth: 420).padding(.horizontal, 28).padding(.top, 48).padding(.bottom, 32) + .frame(maxWidth: .infinity) + } + .background(Theme.bg.ignoresSafeArea()) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Sign out") { model.signOut() }.foregroundStyle(Theme.textMuted) + } + ToolbarItem(placement: .topBarTrailing) { + Button { showDetails = true } label: { Image(systemName: "info.circle") } + .accessibilityLabel("Connection details").foregroundStyle(Theme.textMuted) + } + } + .sheet(isPresented: $showDetails) { EncryptionView() } + .sheet(isPresented: $showRecovery) { + NavigationStack { + Form { + Section { + Text("Use the recovery key you saved when you first set up private chats.") + TextField("Recovery key", text: $recoveryKey, axis: .vertical) + .textInputAutocapitalization(.characters).autocorrectionDisabled() + if model.vaultStatus?.fingerprint == nil { + TextField("Fingerprint from your recovery file", text: $fingerprint) + .textInputAutocapitalization(.never).autocorrectionDisabled() + } + Button("Restore access") { + run { try await model.recoverVault(kit: recoveryKey, fingerprintHex: knownFingerprint) } + }.disabled(working || recoveryKey.count < 55 || knownFingerprint.count != 64) + if let error { Text(error).foregroundStyle(Theme.danger) } + } + } + .navigationTitle("Restore your chats").navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Cancel") { showRecovery = false } } } + } + } + } + .onAppear { model.refreshVault() } + } + + private func run(_ operation: @escaping () async throws -> Void) { + error = nil + working = true + Task { + do { try await operation() } + catch { self.error = "Couldn’t connect. Check your connection and try again. You can find more information in Connection details." } + working = false + } + } +} diff --git a/apps/ios/Zeron/Views/HomeView.swift b/apps/ios/Zeron/Views/HomeView.swift index 28e88c7ba..c20f342ab 100644 --- a/apps/ios/Zeron/Views/HomeView.swift +++ b/apps/ios/Zeron/Views/HomeView.swift @@ -15,6 +15,7 @@ struct HomeView: View { @Environment(AppModel.self) private var model @State private var path: [Route] = [] @State private var showNewSpace = false + @State private var showEncryption = false // "" = All. Sticky across launches; falls back to All if the space is gone. @AppStorage("homeSpaceFilter") private var spaceFilter: String = "" @@ -100,12 +101,18 @@ struct HomeView: View { if model.demo != nil { Text("Demo mode") } + if model.demo == nil { + Button("Encryption…") { showEncryption = true } + } Button("Sign out", role: .destructive) { model.signOut() } } label: { Image(systemName: "person.circle") } } } + .sheet(isPresented: $showEncryption) { + EncryptionView() + } .sheet(isPresented: $showNewSpace) { NewSpaceSheet { spaceId in path.append(.space(spaceId)) diff --git a/apps/ios/Zeron/Views/SignInView.swift b/apps/ios/Zeron/Views/SignInView.swift index a6236f389..1c44a358e 100644 --- a/apps/ios/Zeron/Views/SignInView.swift +++ b/apps/ios/Zeron/Views/SignInView.swift @@ -2,8 +2,8 @@ // the secret-bearing exchange delegated to the edge (`POST /auth/exchange`). // The zeron mark on black, one white button — the old mobile app's Gate. // -// Endpoints are fixed to production (the old app's rule: mobile always talks -// to prod; a stale override once broke sign-in in the worst ghost way). +// OAuth always uses production endpoints. Debug builds expose a separate, +// explicit Dev sign-in for a local AUTH_MODE=dev worker. import AuthenticationServices import SwiftUI @@ -33,6 +33,9 @@ struct SignInView: View { @State private var busy = false @State private var error: String? @State private var authSession = AuthSessionCoordinator() + #if DEBUG + @State private var showDevSignIn = false + #endif var body: some View { ZStack { @@ -77,6 +80,15 @@ struct SignInView: View { .disabled(busy) .opacity(busy ? 0.6 : 1) + #if DEBUG + Button("Dev sign in") { showDevSignIn = true } + .font(Theme.sans(13)) + .foregroundStyle(Theme.textMuted) + .frame(minHeight: 44) + .disabled(busy) + .accessibilityIdentifier("dev-sign-in") + #endif + if let error { Text(error) .font(Theme.sans(13)) @@ -90,6 +102,9 @@ struct SignInView: View { .padding(.horizontal, 32) .frame(maxWidth: 480) } + #if DEBUG + .sheet(isPresented: $showDevSignIn) { DevSignInView() } + #endif } /// The AuthKit code flow: system browser session → zeron://callback with @@ -129,6 +144,60 @@ struct SignInView: View { } } +#if DEBUG +private struct DevSignInView: View { + @Environment(AppModel.self) private var model + @Environment(\.dismiss) private var dismiss + @AppStorage("devSignInEdgeURL") private var edge = "" + @AppStorage("devSignInUser") private var user = "mobile-test" + @AppStorage("devSignInOrg") private var org = "mobile-test" + + private var edgeURL: URL? { + guard let parts = URLComponents(string: edge.trimmingCharacters(in: .whitespacesAndNewlines)), + parts.scheme == "http" || parts.scheme == "https", + let host = parts.host, !host.isEmpty, + parts.user == nil, parts.password == nil, parts.query == nil, parts.fragment == nil else { return nil } + return parts.url + } + private var userId: String { user.trimmingCharacters(in: .whitespacesAndNewlines) } + private var orgId: String { org.trimmingCharacters(in: .whitespacesAndNewlines) } + private var validIdentity: Bool { + !userId.isEmpty && !orgId.isEmpty && !userId.contains("@") && !orgId.contains("@") + } + + var body: some View { + NavigationStack { + Form { + Section { + TextField("http://your-mac.local:27640", text: $edge) + .keyboardType(.URL) + .accessibilityLabel("Edge URL") + TextField("User ID", text: $user) + TextField("Organization ID", text: $org) + } header: { Text("Local development server") } footer: { + Text("Use your Mac's .local hostname. On a phone, localhost points to the phone itself. The server must run with AUTH_MODE=dev.") + } + Section { + Button("Connect") { + guard let edgeURL, validIdentity else { return } + model.signInDev(edgeURL: edgeURL, userId: userId, orgId: orgId) + dismiss() + } + .disabled(edgeURL == nil || !validIdentity) + } + } + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .navigationTitle("Dev sign in") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } + } + } + } +} +#endif + // MARK: - Auth session plumbing /// Wraps ASWebAuthenticationSession with a presentation anchor. diff --git a/apps/ios/ZeronTests/MobileVaultLiveTests.swift b/apps/ios/ZeronTests/MobileVaultLiveTests.swift new file mode 100644 index 000000000..df4d40120 --- /dev/null +++ b/apps/ios/ZeronTests/MobileVaultLiveTests.swift @@ -0,0 +1,106 @@ +import Foundation +import XCTest +@testable import Zeron + +/// Run scripts/test-ios-vault-live.sh. Uses the actual iOS vault, URLSession +/// relay and sidecar clients against a Rust host and local workerd DeviceRoom. +final class MobileVaultLiveTests: XCTestCase { + private let directory = URL(fileURLWithPath: "/tmp/comet-mobile-e2e", isDirectory: true) + struct Connection: Decodable { let edge, org, user, relay, fingerprint, chat: String } + struct Echo: Decodable { let text: String } + + @MainActor + func testEnrollmentRelaySidecarsReconnectAndRevocation() async throws { + let file = directory.appendingPathComponent("connection.json") + guard FileManager.default.fileExists(atPath: file.path), + !FileManager.default.fileExists(atPath: directory.appendingPathComponent("done").path) else { throw XCTSkip("Start the opt-in mobile_test_host first") } + let connection = try JSONDecoder().decode(Connection.self, from: Data(contentsOf: file)) + let config = AppConfig(edgeURL: URL(string: connection.edge)!, mode: .dev, + userId: connection.user, orgId: connection.org, deviceId: UUID().uuidString, + deviceName: "iOS live test", devBearer: "\(connection.user)@\(connection.org)") + let fingerprint = try XCTUnwrap(Data(vaultHex: connection.fingerprint, count: 32)) + _ = await config.vault.refresh(client: config.vaultClient) + try await config.vault.enroll(fingerprint: fingerprint, client: config.vaultClient) + var ready = false + for _ in 0..<40 { + let status = await config.vault.refresh(client: config.vaultClient) + if status.phase == .ready { ready = true; break } + try await Task.sleep(for: .milliseconds(250)) + } + XCTAssertTrue(ready, "Desktop must approve the actual phone identity") + guard ready else { return } + // The app starts approved, then must learn about removal on foreground. + let model = AppModel() + model.signInDev(edgeURL: config.edgeURL, userId: connection.user, orgId: connection.org) + for _ in 0..<40 { + if model.vaultStatus?.phase == .ready { break } + try await Task.sleep(for: .milliseconds(250)) + } + XCTAssertEqual(model.vaultStatus?.phase, .ready) + XCTAssertFalse(model.requiresVaultApproval) + defer { model.signOut() } + config.setSyncAccess(.encrypted) + let relay = DeviceRelayClient(deviceId: connection.relay, config: config) + // Concurrent cold calls share one handshake and preserve transport nonce order. + try await withThrowingTaskGroup(of: Void.self) { group in + for i in 0..<8 { + group.addTask { + let text = "encrypted RPC \(i)" + let reply: Echo = try await relay.call(method: "Echo", params: ["text": text]) + XCTAssertEqual(reply.text, text) + } + } + try await group.waitForAll() + } + let stream: AsyncThrowingStream = try await relay.stream(method: "EchoStream", params: [:]) + var streamed: [String] = [] + for try await item in stream { streamed.append(item.text) } + XCTAssertEqual(streamed, ["stream 0", "stream 1", "stream 2"]) + let large = String(repeating: "private chunk ", count: 20_000) + let reply: Echo = try await relay.call(method: "Echo", params: ["text": large]) + XCTAssertEqual(reply.text, large) + let sidecars = SessionSidecars(config: config, chatId: connection.chat, encrypted: true) + let tail = try await sidecars.tail() + let entries = try SessionStore.decodeTail(tail, chatId: connection.chat) + XCTAssertEqual(entries.count, 1) + let output = try await sidecars.blob(ref: "\(connection.chat)/tool-1") + XCTAssertEqual(output, "Full encrypted tool output") + await relay.close() + let reopened: Echo = try await relay.call(method: "Echo", params: ["text": "after reconnect"]) + XCTAssertEqual(reopened.text, "after reconnect") + try Data().write(to: directory.appendingPathComponent("revoke")) + for _ in 0..<40 { + if FileManager.default.fileExists(atPath: directory.appendingPathComponent("revoked").path) { break } + try await Task.sleep(for: .milliseconds(250)) + } + do { + let _: Echo = try await relay.call(method: "Echo", params: ["text": "revoked"]) + XCTFail("Revoked phone must not use the established channel") + } catch { /* Both the established channel and redial fail closed. */ } + let status = await config.vault.refresh(client: config.vaultClient) + XCTAssertEqual(status.phase, .revoked) + model.foregrounded() + for _ in 0..<40 { + if model.vaultStatus?.phase == .revoked { break } + try await Task.sleep(for: .milliseconds(250)) + } + XCTAssertEqual(model.vaultStatus?.phase, .revoked) + XCTAssertTrue(model.requiresVaultApproval, "Removal must replace the reconnecting UI with reapproval") + XCTAssertNil(model.workspace, "Revoked profiles must stop their sync stores") + // Reapproval must use a fresh identity and leave the blocking screen + // only once the desktop has admitted it and delivered its keys. + try FileManager.default.removeItem(at: directory.appendingPathComponent("revoke")) + try await model.enrollVault(fingerprintHex: connection.fingerprint) + for _ in 0..<40 { + model.refreshVault() + if model.vaultStatus?.phase == .ready { break } + try await Task.sleep(for: .milliseconds(250)) + } + XCTAssertEqual(model.vaultStatus?.phase, .ready) + XCTAssertFalse(model.requiresVaultApproval) + XCTAssertNotNil(model.workspace) + await relay.close() + config.invalidate() + try Data().write(to: directory.appendingPathComponent("done")) + } +} diff --git a/apps/ios/ZeronTests/RegistryCoreTests.swift b/apps/ios/ZeronTests/RegistryCoreTests.swift index 8e3ec14db..854496da8 100644 --- a/apps/ios/ZeronTests/RegistryCoreTests.swift +++ b/apps/ios/ZeronTests/RegistryCoreTests.swift @@ -243,3 +243,35 @@ final class RegistryMaxClockTests: XCTestCase { XCTAssertEqual(maxClock(tomb), hlc(9)) } } + +final class RegistryLifecycleProofTests: XCTestCase { + func testProofRidesTheTombstoneAndClearsOnRevival() { + let proof = JSONValue.object(["e1": .string("c2VhbGVk")]) + var del = deleteOp(hlc: hlc(2000)) + del.proof = proof + let gone = applied(applied(nil, upsert()), del) + XCTAssertTrue(gone.deleted) + XCTAssertEqual(gone.delProof, proof) + var newer = deleteOp(hlc: hlc(3000)) + newer.proof = .object(["e1": .string("bmV3ZXI=")]) + let again = applied(gone, newer) + XCTAssertEqual(again.delProof, newer.proof) + XCTAssertFalse(applyOp(again, del).changed) + XCTAssertEqual(rowToSeedOp(again).proof, newer.proof) + let revived = applied(again, upsert(hlc: hlc(4000))) + XCTAssertFalse(revived.deleted) + XCTAssertNil(revived.delProof) + XCTAssertEqual(revived.delHlc, hlc(3000)) + } + + func testValidateOpRefusesProofsOutsideDeletes() { + var op = upsert() + op.proof = .object(["e1": .string("x")]) + XCTAssertEqual(validateOp(op), "bad proof") + var del = deleteOp(hlc: hlc(1)) + del.proof = .string("not an envelope") + XCTAssertEqual(validateOp(del), "bad proof") + del.proof = .object(["e1": .string("x")]) + XCTAssertNil(validateOp(del)) + } +} diff --git a/apps/ios/ZeronTests/SessionSidecarsTests.swift b/apps/ios/ZeronTests/SessionSidecarsTests.swift new file mode 100644 index 000000000..2dafaa022 --- /dev/null +++ b/apps/ios/ZeronTests/SessionSidecarsTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import Zeron + +final class SessionSidecarsTests: XCTestCase { + private func config() -> AppConfig { + AppConfig(edgeURL: URL(string: "http://localhost:27640")!, mode: .dev, + userId: "test", orgId: "test", deviceId: "phone", deviceName: "Phone", devBearer: "test@test") + } + func testForeignAndEscapingBlobReferencesAreRefused() throws { + for ref in ["other/part", "chat/../x", "chat/..", "chat/.", "chat/a?token=x", "chat/a%2fb", "chat/", "/part"] { + XCTAssertThrowsError(try SessionSidecars.blobPart(ref: ref, chatId: "chat")) + } + XCTAssertEqual(try SessionSidecars.blobPart(ref: "chat/tool:1#2.diff", chatId: "chat"), "tool:1#2.diff") + } + func testBlockedProfileNeverRequestsSidecars() async { + let reader = SessionSidecars(config: config(), chatId: "chat", encrypted: true, transport: { _ in + XCTFail("Blocked profile must not dial") + return (Data(), 200) + }) + do { _ = try await reader.tail(); XCTFail("Expected access refusal") } catch {} + } + func testEncryptedReaderRefusesPlaintextAndLegacyRoute() async { + let config = config() + config.setSyncAccess(.encrypted) + let reader = SessionSidecars(config: config, chatId: "chat", encrypted: true, transport: { request in + XCTAssertEqual(request.url?.path, "/chat2/chat-e1/tail") + XCTAssertNil(request.url?.query) + return (Data(#"{"chatId":"chat","messages":[]}"#.utf8), 200) + }) + do { _ = try await reader.tail(); XCTFail("Expected plaintext refusal") } catch {} + let legacy = SessionSidecars(config: config, chatId: "chat", encrypted: false, transport: { _ in + XCTFail("Enrolled profile must never read legacy sidecars") + return (Data(), 200) + }) + do { _ = try await legacy.tail(); XCTFail("Expected refusal") } catch {} + } + func testTailDecodeKeepsOutputRefsAndRejectsWrongChat() throws { + let json = Data(#"{"chatId":"chat","schemaVersion":1,"totalMessages":1,"messages":[{"id":"m","role":"assistant","createdAt":1000,"deviceId":"host","parts":[{"id":"p","kind":"tool","call":{"kind":"exec","command":"pwd"},"resolved":true,"isError":false,"outputRef":"chat/p","diffRef":"chat/p.diff"}]}]}"#.utf8) + let entries = try SessionStore.decodeTail(json, chatId: "chat") + guard case .tool(_, let call, _, let resolved) = entries[0].parts[0] else { return XCTFail("Missing tool") } + XCTAssertTrue(resolved) + XCTAssertEqual(call.string("outputRef"), "chat/p") + XCTAssertEqual(call.string("diffRef"), "chat/p.diff") + XCTAssertThrowsError(try SessionStore.decodeTail(json, chatId: "other")) + } +} diff --git a/apps/ios/ZeronTests/VaultChannelTests.swift b/apps/ios/ZeronTests/VaultChannelTests.swift new file mode 100644 index 000000000..1efc7f085 --- /dev/null +++ b/apps/ios/ZeronTests/VaultChannelTests.swift @@ -0,0 +1,64 @@ +import CryptoKit +import XCTest +@testable import Zeron + +final class VaultChannelTests: XCTestCase { + struct Fixture: Decodable { + struct Frame: Decodable { let plaintext, outgoing, incoming: String } + let first, second, third, peerKey: String + let frames: [Frame] + } + private func fixture() throws -> Fixture { + let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + return try JSONDecoder().decode(Fixture.self, from: Data(contentsOf: + root.appendingPathComponent("crates/crypto/tests/fixtures/channel.json"))) + } + private func bytes(_ hex: String) -> Data { + let raw = Array(hex.utf8) + func nibble(_ byte: UInt8) -> UInt8 { byte <= 57 ? byte - 48 : byte - 87 } + return Data(stride(from: 0, to: raw.count, by: 2).map { + nibble(raw[$0]) * 16 + nibble(raw[$0 + 1]) + }) + } + private func handshake(vault: UInt8 = 1) throws -> VaultChannelHandshake { + try VaultChannelHandshake(deviceId: Data(repeating: 7, count: 16), staticKey: Data(repeating: 3, count: 32), + vaultId: Data(repeating: vault, count: 16), generation: Data(repeating: 2, count: 16), + ephemeralKey: .init(rawRepresentation: Data(repeating: 5, count: 32))) + } + func testSnowHandshakeAndChunkedTransportInBothDirections() throws { + let f = try fixture() + let hs = try handshake() + XCTAssertEqual(hs.first, bytes(f.first)) + let (third, channel) = try hs.finish(bytes(f.second)) { id, key in + id == Data(repeating: 8, count: 16) && key == self.bytes(f.peerKey) + } + XCTAssertEqual(third, bytes(f.third)) + for frame in f.frames { + XCTAssertEqual(try channel.seal(bytes(frame.plaintext)), bytes(frame.outgoing)) + XCTAssertEqual(try channel.open(bytes(frame.incoming)), bytes(frame.plaintext)) + } + XCTAssertThrowsError(try hs.finish(bytes(f.second)) { _, _ in true }) + XCTAssertThrowsError(try channel.open(bytes(f.frames[0].incoming))) + XCTAssertThrowsError(try channel.seal(Data())) + } + func testForeignVaultUnapprovedPeerAndDamagedHandshakeFailClosed() throws { + let f = try fixture() + XCTAssertThrowsError(try handshake(vault: 9).finish(bytes(f.second)) { _, _ in true }) + XCTAssertThrowsError(try handshake().finish(bytes(f.second)) { _, _ in false }) + var damaged = bytes(f.second) + damaged[70] ^= 1 + XCTAssertThrowsError(try handshake().finish(damaged) { _, _ in true }) + XCTAssertThrowsError(try handshake().finish(Data()) { _, _ in true }) + } + func testMalformedTransportRetiresChannel() throws { + let f = try fixture() + for damage in [Data(), bytes(f.frames[0].incoming).dropLast(), bytes(f.frames[0].incoming) + Data([0])] { + let (_, channel) = try handshake().finish(bytes(f.second)) { _, _ in true } + XCTAssertThrowsError(try channel.open(Data(damage))) + XCTAssertThrowsError(try channel.open(bytes(f.frames[0].incoming))) + } + let (_, channel) = try handshake().finish(bytes(f.second)) { _, _ in true } + XCTAssertThrowsError(try channel.seal(Data(repeating: 0, count: VaultChannel.maximum + 1))) + } +} diff --git a/apps/ios/ZeronTests/VaultContentTests.swift b/apps/ios/ZeronTests/VaultContentTests.swift new file mode 100644 index 000000000..9e650b715 --- /dev/null +++ b/apps/ios/ZeronTests/VaultContentTests.swift @@ -0,0 +1,95 @@ +import CryptoKit +import XCTest +@testable import Zeron + +final class VaultContentTests: XCTestCase { + private func setup() throws -> (VaultRecordBinding, VaultContentKey, VaultDeviceSigner) { + let binding = VaultRecordBinding( + kind: .content, vaultId: Data(repeating: 1, count: 16), generation: Data(repeating: 2, count: 16), epoch: 1, + objectId: Data(repeating: 3, count: 16), authorId: Data(repeating: 4, count: 16), membershipHash: Data(repeating: 5, count: 32) + ) + let key = try VaultContentKey(scope: VaultKeyScope(binding), identifier: Data(repeating: 6, count: 16), bytes: Data(repeating: 7, count: 32)) + let signer = try VaultDeviceSigner(authorId: binding.authorId, seed: Data(repeating: 8, count: 32)) + return (binding, key, signer) + } + + func testEncryptedContentRoundTripAndFreshSeals() throws { + let (binding, key, signer) = try setup() + let plaintext = Data("private transcript".utf8) + Data([0, 255]) + let first = try VaultContentCrypto.seal(binding: binding, purpose: .chatUpdate, key: key, signer: signer, plaintext: plaintext, maxPlaintextBytes: 1024) + let retry = first.encoded + let second = try VaultContentCrypto.seal(binding: binding, purpose: .chatUpdate, key: key, signer: signer, plaintext: plaintext, maxPlaintextBytes: 1024) + let opened = try VaultContentCrypto.open((Data([255]) + first.encoded).dropFirst(), expected: binding, purpose: .chatUpdate, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: 1024) + XCTAssertEqual(opened.plaintext, plaintext) + XCTAssertEqual(opened.revisionId, first.revisionId) + XCTAssertEqual(retry, first.encoded) + XCTAssertNotEqual(first.revisionId, second.revisionId) + XCTAssertNotEqual(first.encoded, second.encoded) + XCTAssertNil(first.encoded.range(of: plaintext)) + XCTAssertEqual(String(reflecting: key), "ContentKey([REDACTED])") + XCTAssertEqual(String(reflecting: signer), "DeviceSigner([REDACTED])") + XCTAssertEqual(String(reflecting: opened), "OpenedContent([REDACTED])") + } + + func testEncryptedContentRejectsWrongScopePurposeAuthorAndLimits() throws { + let (binding, key, signer) = try setup() + let sealed = try VaultContentCrypto.seal(binding: binding, purpose: .tail, key: key, signer: signer, plaintext: Data([1]), maxPlaintextBytes: 1) + XCTAssertThrowsError(try VaultContentCrypto.open(sealed.encoded, expected: binding, purpose: .blob, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: 1)) { + XCTAssertEqual($0 as? VaultContentError, .wrongPurpose) + } + var changed = binding + changed.epoch = 2 + XCTAssertThrowsError(try VaultContentCrypto.seal(binding: changed, purpose: .tail, key: key, signer: signer, plaintext: Data(), maxPlaintextBytes: 0)) { + XCTAssertEqual($0 as? VaultContentError, .wrongScope) + } + changed = binding + changed.authorId = Data(repeating: 9, count: 16) + XCTAssertThrowsError(try VaultContentCrypto.seal(binding: changed, purpose: .tail, key: key, signer: signer, plaintext: Data(), maxPlaintextBytes: 0)) { + XCTAssertEqual($0 as? VaultContentError, .wrongAuthor) + } + let wrongKey = try VaultContentKey(scope: key.scope, identifier: key.identifier, bytes: Data(repeating: 9, count: 32)) + XCTAssertThrowsError(try VaultContentCrypto.open(sealed.encoded, expected: binding, purpose: .tail, key: wrongKey, trustedPublicKey: signer.publicKey, maxPlaintextBytes: 1)) { + XCTAssertEqual($0 as? VaultContentError, .crypto(.authenticationFailed)) + } + for maximum in [-1, 0, Int.max] { + XCTAssertThrowsError(try VaultContentCrypto.seal(binding: binding, purpose: .tail, key: key, signer: signer, plaintext: Data([1]), maxPlaintextBytes: maximum)) { + XCTAssertEqual($0 as? VaultContentError, .sizeLimitExceeded) + } + } + } + + func testEncryptedContentRequiresAEADAfterValidSignature() throws { + let (binding, key, signer) = try setup() + let sealed = try VaultContentCrypto.seal(binding: binding, purpose: .chatUpdate, key: key, signer: signer, plaintext: Data("private".utf8), maxPlaintextBytes: 128) + let verified = try VaultUnverifiedRecord.parse(sealed.encoded, maxPayloadBytes: 272).verify(expected: binding, trustedPublicKey: signer.publicKey) + let signingKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(repeating: 8, count: 32)) + var corrupted = verified.payload + corrupted[corrupted.index(before: corrupted.endIndex)] ^= 1 + let input = try VaultRecordCodec.signingBytes(binding: binding, revisionId: sealed.revisionId, payload: corrupted, maxPayloadBytes: 272) + let encoded = try VaultRecordCodec.encodeSigned(binding: binding, revisionId: sealed.revisionId, payload: corrupted, signature: signingKey.signature(for: input), maxPayloadBytes: 272) + XCTAssertThrowsError(try VaultContentCrypto.open(encoded, expected: binding, purpose: .chatUpdate, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: 128)) { + XCTAssertEqual($0 as? VaultContentError, .crypto(.authenticationFailed)) + } + var changed = binding + changed.membershipHash = Data(repeating: 9, count: 32) + let changedInput = try VaultRecordCodec.signingBytes(binding: changed, revisionId: sealed.revisionId, payload: verified.payload, maxPayloadBytes: 272) + let changedRecord = try VaultRecordCodec.encodeSigned(binding: changed, revisionId: sealed.revisionId, payload: verified.payload, signature: signingKey.signature(for: changedInput), maxPayloadBytes: 272) + XCTAssertThrowsError(try VaultContentCrypto.open(changedRecord, expected: changed, purpose: .chatUpdate, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: 128)) { + XCTAssertEqual($0 as? VaultContentError, .crypto(.authenticationFailed)) + } + } + + func testContentKeyGenerationAndEmptyPayloads() throws { + let (binding, key, signer) = try setup() + let first = try VaultContentKey.generate(scope: key.scope) + let second = try VaultContentKey.generate(scope: key.scope) + XCTAssertNotEqual(first.identifier, second.identifier) + XCTAssertNotEqual(first.exposeSecret(), second.exposeSecret()) + for raw: UInt64 in 1...8 { + let purpose = try XCTUnwrap(VaultContentPurpose(rawValue: raw)) + let sealed = try VaultContentCrypto.seal(binding: binding, purpose: purpose, key: key, signer: signer, plaintext: Data(), maxPlaintextBytes: 0) + XCTAssertTrue(try VaultContentCrypto.open(sealed.encoded, expected: binding, purpose: purpose, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: 0).plaintext.isEmpty) + } + XCTAssertThrowsError(try VaultContentKey(scope: key.scope, identifier: key.identifier, bytes: Data(repeating: 0, count: 31))) + } +} diff --git a/apps/ios/ZeronTests/VaultControlPlaneTests.swift b/apps/ios/ZeronTests/VaultControlPlaneTests.swift new file mode 100644 index 000000000..46783aacf --- /dev/null +++ b/apps/ios/ZeronTests/VaultControlPlaneTests.swift @@ -0,0 +1,167 @@ +import CryptoKit +import XCTest +@testable import Zeron + +/// Cross-language conformance against the Rust-generated fixture +/// (`crates/crypto/tests/fixtures/vault.json`): the membership chain, hashes, +/// epochs, keyring/object-key envelopes, the sealed chat record, the +/// recovery kit, and the enrollment proof / pairing code must all agree. +final class VaultControlPlaneTests: XCTestCase { + private struct Fixture: Decodable { + struct Device: Decodable { let id, signingSeed, signingKey, encryptionSecret, encryptionKey: String } + struct Enrollment: Decodable { let requestId, deviceId, signingKey, encryptionKey, proof, pairingCode: String } + let vaultId, generation, orgId, userId, profileHash: String + let recoverySecret, recoveryKit, recoverySigningKey, recoveryEncryptionKey, recoveryAuthorityId: String + let deviceA, deviceB: Device + let membership, membershipHashes: [String] + let epochsAfter: [UInt64] + let keyringEnvelopeB, keyringEpoch1, objectId, objectKeyEnvelope, objectKeyId, objectKey: String + let chatRecord, chatPlaintext: String + let registryLifecycleRecord, registryLifecyclePlaintext: String + let enrollment: Enrollment + } + + private func loadFixture() throws -> Fixture { + let url = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("crates/crypto/tests/fixtures/vault.json") + return try JSONDecoder().decode(Fixture.self, from: Data(contentsOf: url)) + } + + private func hex(_ text: String) -> Data { + var out = Data(capacity: text.count / 2) + var index = text.startIndex + while index < text.endIndex { + let next = text.index(index, offsetBy: 2) + out.append(UInt8(text[index.. String { data.map { String(format: "%02x", $0) }.joined() } + private func b64(_ text: String) -> Data { Data(base64Encoded: text)! } + + private func chain(_ fixture: Fixture) throws -> [VaultMembershipState] { + let records = fixture.membership.map(b64) + var states = [try VaultMembershipState.fromGenesis( + records[0], expectedVaultId: hex(fixture.vaultId), expectedGeneration: hex(fixture.generation), + expectedProfileHash: VaultPolicy.profileHash(orgId: fixture.orgId, userId: fixture.userId) + )] + for record in records.dropFirst() { states.append(try states.last!.apply(record)) } + return states + } + + func testMembershipChainAgreesWithRust() throws { + let fixture = try loadFixture() + XCTAssertEqual(hex(VaultPolicy.profileHash(orgId: fixture.orgId, userId: fixture.userId)), fixture.profileHash) + let states = try chain(fixture) + XCTAssertEqual(states.map { hex($0.hash) }, fixture.membershipHashes) + XCTAssertEqual(states.map(\.epoch), fixture.epochsAfter) + let head = states.last! + XCTAssertNotNil(head.activeDevice(hex(fixture.deviceA.id))) + XCTAssertNil(head.activeDevice(hex(fixture.deviceB.id))) + XCTAssertEqual(head.device(hex(fixture.deviceB.id))?.status, .revoked) + XCTAssertEqual(hex(head.recoveryAuthorityId), fixture.recoveryAuthorityId) + // Replays, forks, and mutations fail closed. + let records = fixture.membership.map(b64) + XCTAssertThrowsError(try states[1].apply(records[1])) + XCTAssertThrowsError(try states[0].apply(records[2])) + for index in [0, 5, 40, records[1].count - 1] { + var damaged = records[1] + damaged[damaged.startIndex + index] ^= 1 + XCTAssertThrowsError(try states[0].apply(damaged), "byte \(index)") + } + XCTAssertThrowsError(try VaultMembershipState.fromGenesis( + records[0], expectedVaultId: hex(fixture.vaultId), expectedGeneration: hex(fixture.generation), + expectedProfileHash: Data(repeating: 0, count: 32) + )) { XCTAssertEqual($0 as? VaultPolicyError, .wrongProfile) } + } + + func testKeyringAndObjectKeyEnvelopesOpenTheChatRecord() throws { + let fixture = try loadFixture() + let states = try chain(fixture) + let added = states[1] + let aId = hex(fixture.deviceA.id) + let aPublic = hex(fixture.deviceA.signingKey) + let bKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: hex(fixture.deviceB.encryptionSecret)) + let keyring = try VaultEnvelope.openKeyring( + b64(fixture.keyringEnvelopeB), + expected: added.envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: 1, authorId: aId), + recipientKind: .device, recipientId: hex(fixture.deviceB.id), recipientKey: bKey, trustedPublicKey: aPublic + ) + XCTAssertEqual(hex(keyring.epochKey(1)!), fixture.keyringEpoch1) + XCTAssertEqual(try VaultKeyring.decode(keyring.encode()), keyring) + // The wrong recipient or a stale head cannot open it. + XCTAssertThrowsError(try VaultEnvelope.openKeyring( + b64(fixture.keyringEnvelopeB), + expected: added.envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: 1, authorId: aId), + recipientKind: .recovery, recipientId: hex(fixture.deviceB.id), recipientKey: bKey, trustedPublicKey: aPublic + )) + XCTAssertThrowsError(try VaultEnvelope.openKeyring( + b64(fixture.keyringEnvelopeB), + expected: states[2].envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: 1, authorId: aId), + recipientKind: .device, recipientId: hex(fixture.deviceB.id), recipientKey: bKey, trustedPublicKey: aPublic + )) + let objectId = hex(fixture.objectId) + let objectKey = try VaultEnvelope.unwrapObjectKey( + b64(fixture.objectKeyEnvelope), + expected: added.envelopeBinding(objectId: objectId, epoch: 1, authorId: aId), + epochKey: keyring.epochKey(1)!, trustedPublicKey: aPublic + ) + XCTAssertEqual(hex(objectKey.identifier), fixture.objectKeyId) + XCTAssertEqual(hex(objectKey.exposeSecret()), fixture.objectKey) + let opened = try VaultContentCrypto.open( + b64(fixture.chatRecord), expected: added.contentBinding(objectId: objectId, authorId: aId), + purpose: .chatUpdate, key: objectKey, trustedPublicKey: aPublic, maxPlaintextBytes: 1024 + ) + XCTAssertEqual(String(decoding: opened.plaintext, as: UTF8.self), fixture.chatPlaintext) + // After the revocation the head epoch moved on: the old record no + // longer matches the current binding. + XCTAssertThrowsError(try VaultContentCrypto.open( + b64(fixture.chatRecord), expected: states[2].contentBinding(objectId: objectId, authorId: aId), + purpose: .chatUpdate, key: objectKey, trustedPublicKey: aPublic, maxPlaintextBytes: 1024 + )) + // A registry row lifecycle proof (purpose 9) opens as such and never + // as a field value: the purpose is bound into the record. + let lifecycle = try VaultContentCrypto.open( + b64(fixture.registryLifecycleRecord), expected: added.contentBinding(objectId: objectId, authorId: aId), + purpose: .registryLifecycle, key: objectKey, trustedPublicKey: aPublic, maxPlaintextBytes: 1024 + ) + XCTAssertEqual(String(decoding: lifecycle.plaintext, as: UTF8.self), fixture.registryLifecyclePlaintext) + XCTAssertThrowsError(try VaultContentCrypto.open( + b64(fixture.registryLifecycleRecord), expected: added.contentBinding(objectId: objectId, authorId: aId), + purpose: .registryField, key: objectKey, trustedPublicKey: aPublic, maxPlaintextBytes: 1024 + )) + } + + func testRecoveryKitAndEnrollmentAgreeWithRust() throws { + let fixture = try loadFixture() + let secret = try VaultRecoverySecret(kit: fixture.recoveryKit) + XCTAssertEqual(hex(secret.secret), fixture.recoverySecret) + XCTAssertEqual(secret.kit, fixture.recoveryKit) + XCTAssertEqual(hex(try secret.signingKey().publicKey.rawRepresentation), fixture.recoverySigningKey) + XCTAssertEqual(hex(try secret.encryptionKey().publicKey.rawRepresentation), fixture.recoveryEncryptionKey) + XCTAssertEqual(hex(try secret.authorityId()), fixture.recoveryAuthorityId) + XCTAssertEqual(try VaultRecoverySecret(kit: fixture.recoveryKit.lowercased().replacingOccurrences(of: "-", with: " ")).secret, secret.secret) + var damaged = Array(fixture.recoveryKit) + damaged[0] = damaged[0] == "A" ? "B" : "A" + XCTAssertThrowsError(try VaultRecoverySecret(kit: String(damaged))) { + XCTAssertEqual($0 as? VaultRecoveryError, .checksumMismatch) + } + let request = VaultEnrollmentRequest( + vaultId: hex(fixture.vaultId), requestId: hex(fixture.enrollment.requestId), + deviceId: hex(fixture.enrollment.deviceId), signingKey: hex(fixture.enrollment.signingKey), + encryptionKey: hex(fixture.enrollment.encryptionKey) + ) + XCTAssertNoThrow(try request.verify(proof: hex(fixture.enrollment.proof))) + let genesis = hex(fixture.membershipHashes[0]) + XCTAssertEqual(request.pairingCode(genesisHash: genesis), fixture.enrollment.pairingCode) + var swapped = request + swapped.encryptionKey = Data(repeating: 9, count: 32) + XCTAssertThrowsError(try swapped.verify(proof: hex(fixture.enrollment.proof))) + XCTAssertNotEqual(swapped.pairingCode(genesisHash: genesis), fixture.enrollment.pairingCode) + XCTAssertNotEqual(request.pairingCode(genesisHash: Data(repeating: 0, count: 32)), fixture.enrollment.pairingCode) + } +} diff --git a/apps/ios/ZeronTests/VaultCryptoTests.swift b/apps/ios/ZeronTests/VaultCryptoTests.swift new file mode 100644 index 000000000..f1fdba6e6 --- /dev/null +++ b/apps/ios/ZeronTests/VaultCryptoTests.swift @@ -0,0 +1,251 @@ +import CryptoKit +import XCTest +@testable import Zeron + +final class VaultCryptoTests: XCTestCase { + private func signedRecordSample() throws -> (Data, Data, VaultRecordBinding) { + let binding = VaultRecordBinding( + kind: .content, vaultId: Data(repeating: 1, count: 16), generation: Data(repeating: 2, count: 16), epoch: 24, + objectId: Data(repeating: 3, count: 16), authorId: Data(repeating: 4, count: 16), membershipHash: Data(repeating: 6, count: 32) + ) + let revision = Data(repeating: 5, count: 16) + let payload = Data([0, 255, 16, 32]) + let key = Curve25519.Signing.PrivateKey() + let input = try VaultRecordCodec.signingBytes(binding: binding, revisionId: revision, payload: payload, maxPayloadBytes: 4) + let encoded = try VaultRecordCodec.encodeSigned( + binding: binding, revisionId: revision, payload: payload, signature: key.signature(for: input), maxPayloadBytes: 4 + ) + return (encoded, key.publicKey.rawRepresentation, binding) + } + + func testEd25519PrechecksPreserveSlicedValidSignatures() throws { + let signer = Curve25519.Signing.PrivateKey() + let message = Data("synthetic signature precheck".utf8) + let key = (Data([255]) + signer.publicKey.rawRepresentation).dropFirst() + let signature = (Data([255]) + (try signer.signature(for: message))).dropFirst() + let originalKey = key + let originalSignature = signature + XCTAssertTrue(VaultCrypto.passesEd25519PointEncodingPrecheck(key)) + XCTAssertTrue(VaultCrypto.passesEd25519PointEncodingPrecheck(signature.prefix(32))) + XCTAssertTrue(VaultCrypto.passesEd25519ScalarEncodingPrecheck(signature.suffix(32))) + XCTAssertNoThrow(try VaultCrypto.verifyEd25519(publicKey: key, message: message, signature: signature)) + XCTAssertEqual(key, originalKey) + XCTAssertEqual(signature, originalSignature) + let order = Data([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + ] + Array(repeating: 0, count: 15) + [0x10]) + XCTAssertFalse(VaultCrypto.passesEd25519ScalarEncodingPrecheck(order)) + var noncanonical = signature + noncanonical.replaceSubrange(noncanonical.index(noncanonical.endIndex, offsetBy: -32).. }, } #[derive(Subcommand)] @@ -189,6 +220,27 @@ fn main() -> anyhow::Result<()> { let runtime = tokio::runtime::Runtime::new()?; runtime.block_on(update_cli::update(&edge_url_from_env(), check)) } + Some(Command::Vault { command }) => { + let runtime = tokio::runtime::Runtime::new()?; + let port = engine_config_from_env().ipc_port; + runtime.block_on(async move { + match command { + VaultCommand::ConfirmRecovery => vault_cli::confirm_recovery(port).await, + VaultCommand::Status => vault_cli::status(port).await, + VaultCommand::Setup => vault_cli::setup(port).await, + VaultCommand::Pair => vault_cli::pair(port).await, + VaultCommand::Requests => vault_cli::requests(port).await, + VaultCommand::Approve { request_id, code } => { + vault_cli::approve(port, &request_id, &code).await + } + VaultCommand::Reject { request_id } => { + vault_cli::reject(port, &request_id).await + } + VaultCommand::Revoke { device_id } => vault_cli::revoke(port, &device_id).await, + VaultCommand::Recover { kit } => vault_cli::recover(port, kit).await, + } + }) + } Some(Command::Daemon { command }) => match command { DaemonCommand::Install => daemon::install(&engine_config_from_env().data_dir), DaemonCommand::Uninstall => daemon::uninstall(), diff --git a/apps/zeron/src/vault_cli.rs b/apps/zeron/src/vault_cli.rs new file mode 100644 index 000000000..9efe46328 --- /dev/null +++ b/apps/zeron/src/vault_cli.rs @@ -0,0 +1,256 @@ +//! `zeron vault …` — the headless surface for encrypted sync (RFC 0001 §4.5): +//! status, setup with a recovery kit, pairing by comparison code, approving +//! and removing devices, and recovery. Every command talks to the RUNNING +//! engine over localhost IPC, so the keys stay inside the engine process; +//! nothing here prints or accepts private key material, only the recovery +//! kit the user is asked to save. + +use std::io::{IsTerminal, Read, Write}; + +use serde_json::Value; +use zeron_rpc::methods; + +async fn client(ipc_port: u16) -> anyhow::Result { + zeron_rpc::connect_ws(&format!("ws://127.0.0.1:{ipc_port}")) + .await + .map_err(|e| { + anyhow::anyhow!("no engine listening on 127.0.0.1:{ipc_port} ({e}) — is zeron running?") + }) +} + +fn field<'a>(value: &'a Value, key: &str) -> &'a str { + value.get(key).and_then(Value::as_str).unwrap_or("") +} + +fn print_status(status: &Value) { + let phase = field(status, "phase"); + let line = match phase { + "ready" => "encrypted (this device is approved)".to_string(), + "notEnrolled" => { + if status.get("remoteVault").and_then(Value::as_bool) == Some(true) { + "not approved — run `zeron vault pair` on this device, then approve it elsewhere, or `zeron vault recover`".into() + } else { + "not set up — run `zeron vault setup`".into() + } + } + "pending" => format!( + "waiting for approval — compare code {} on an approved device", + field(status, "pairingCode") + ), + "locked" => format!("locked — {}", field(status, "reason")), + "keyUpdateRequired" => "waiting for encryption keys from another device".into(), + "verificationFailed" => format!("sync paused — {}", field(status, "reason")), + "revoked" => "removed from the vault".into(), + "unavailable" => format!("not available — {}", field(status, "reason")), + other => other.to_string(), + }; + println!("Encryption: {line}"); + if let Some(fingerprint) = status.get("genesisHash").and_then(Value::as_str) { + println!("Vault fingerprint: {fingerprint}"); + } + if let Some(epoch) = status.get("epoch").and_then(Value::as_u64) { + println!("Key epoch: {epoch}"); + } + println!( + "Key store: {}", + match field(status, "protection") { + "keychain" => "macOS Keychain", + "systemdCredential" => "systemd credential (unattended)", + "keyFile" => "ZERON_VAULT_KEY_FILE (unattended)", + _ => "none", + } + ); + if let Some(devices) = status.get("devices").and_then(Value::as_array) + && !devices.is_empty() + { + println!("Devices:"); + for device in devices { + println!( + " {} {}{}", + field(device, "deviceId"), + field(device, "status"), + if device.get("thisDevice").and_then(Value::as_bool) == Some(true) { + " (this device)" + } else { + "" + } + ); + } + } +} + +pub async fn status(ipc_port: u16) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + let status = client + .call(methods::VAULT_REFRESH, serde_json::json!({})) + .await + .map_err(|e| anyhow::anyhow!("VaultRefresh failed: {e}"))?; + print_status(&status); + Ok(()) +} + +pub async fn confirm_recovery(ipc_port: u16) -> anyhow::Result<()> { + client(ipc_port) + .await? + .call(methods::VAULT_CONFIRM_RECOVERY, serde_json::json!({})) + .await?; + println!("Recovery kit confirmed. The vault is ready."); + Ok(()) +} + +pub async fn setup(ipc_port: u16) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + let kit = client + .call(methods::VAULT_SETUP, serde_json::json!({})) + .await + .map_err(|e| anyhow::anyhow!("setup failed: {e}"))?; + println!( + "Vault prepared. Encrypted writes remain paused until you save and confirm the recovery kit.\n" + ); + println!("Recovery key (save it in a password manager now):\n"); + println!(" {}\n", field(&kit, "kit")); + println!("Recovery file (save alongside the key):\n"); + println!( + "{}\n", + serde_json::to_string_pretty(kit.get("recoveryFile").unwrap_or(&Value::Null))? + ); + println!( + "If you lose every approved device and your recovery key, we cannot recover your \ + encrypted data. Resetting your account password will not restore access.\n\n\ + After saving both, run `zeron vault confirm-recovery`." + ); + Ok(()) +} + +/// Request approval and wait (polling) until an approved device decides. +pub async fn pair(ipc_port: u16) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + let request = client + .call(methods::VAULT_REQUEST_ENROLLMENT, serde_json::json!({})) + .await + .map_err(|e| anyhow::anyhow!("enrollment request failed: {e}"))?; + println!( + "Approve this device from an approved device (Settings → Encryption or `zeron vault approve`).\n\ + Comparison code: {}\n\ + Approve only if the other device shows exactly this code. Waiting…", + field(&request, "pairingCode") + ); + loop { + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + let status = client + .call(methods::VAULT_REFRESH, serde_json::json!({})) + .await + .map_err(|e| anyhow::anyhow!("VaultRefresh failed: {e}"))?; + match field(&status, "phase") { + "pending" => continue, + "ready" => { + println!("Approved. Encrypted sync is active on this device."); + return Ok(()); + } + other => { + anyhow::bail!("the request ended without approval (state: {other})"); + } + } + } +} + +pub async fn requests(ipc_port: u16) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + let list = client + .call(methods::VAULT_PENDING_REQUESTS, serde_json::json!({})) + .await + .map_err(|e| anyhow::anyhow!("VaultPendingRequests failed: {e}"))?; + let requests = list + .get("requests") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + if requests.is_empty() { + println!("No devices are waiting for approval."); + return Ok(()); + } + for request in requests { + println!( + "{} device {} code {}", + field(&request, "requestId"), + field(&request, "deviceId"), + field(&request, "pairingCode") + ); + } + println!("\nApprove with: zeron vault approve "); + Ok(()) +} + +pub async fn approve(ipc_port: u16, request_id: &str, code: &str) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + println!( + "This grants the device full access to every synced session, file and workspace detail, \ + and the ability to approve or remove other devices." + ); + client + .call( + methods::VAULT_APPROVE, + serde_json::json!({ "requestId": request_id, "code": code }), + ) + .await + .map_err(|e| anyhow::anyhow!("approval failed: {e}"))?; + println!("Approved."); + Ok(()) +} + +pub async fn reject(ipc_port: u16, request_id: &str) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + client + .call( + methods::VAULT_REJECT, + serde_json::json!({ "requestId": request_id }), + ) + .await + .map_err(|e| anyhow::anyhow!("reject failed: {e}"))?; + println!("Rejected."); + Ok(()) +} + +pub async fn revoke(ipc_port: u16, device_id: &str) -> anyhow::Result<()> { + let client = client(ipc_port).await?; + client + .call( + methods::VAULT_REVOKE, + serde_json::json!({ "deviceId": device_id }), + ) + .await + .map_err(|e| anyhow::anyhow!("revoke failed: {e}"))?; + println!( + "Removed. This stops future sync access after the change takes effect. It cannot erase \ + information the device already downloaded." + ); + Ok(()) +} + +/// Recover with the kit: read it from the argument, or from stdin (never a +/// command-line secret in shell history when stdin is available). +pub async fn recover(ipc_port: u16, kit: Option) -> anyhow::Result<()> { + let kit = match kit { + Some(kit) => kit, + None => { + if std::io::stdin().is_terminal() { + print!("Recovery key: "); + std::io::stdout().flush()?; + } + let mut text = String::new(); + std::io::stdin().read_to_string(&mut text)?; + text + } + }; + let kit = kit.trim().to_string(); + if kit.is_empty() { + anyhow::bail!("no recovery key given"); + } + let client = client(ipc_port).await?; + client + .call(methods::VAULT_RECOVER, serde_json::json!({ "kit": kit })) + .await + .map_err(|e| anyhow::anyhow!("recovery failed: {e}"))?; + println!("Recovered. This device is approved under a fresh key epoch."); + Ok(()) +} diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml new file mode 100644 index 000000000..2d77dedf7 --- /dev/null +++ b/crates/crypto/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "zeron-crypto" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +ring = { version = "=0.17.14", default-features = false, features = ["alloc"] } +zeroize = { version = "=1.9.0", default-features = false, features = ["alloc"] } +# X25519 for HPKE (RFC 9180 DHKEM) recipient envelopes: ring exposes only +# single-use ephemeral agreement keys, so static recipient decapsulation +# needs a curve library. Reviewed 4.1.3 archive (checksum 97fb8b7c…b2be): +# cfg-only build script, no process/network/filesystem access. +curve25519-dalek = { version = "=4.1.3", default-features = false, features = ["zeroize"] } +# Noise XX device channel (RFC 0001 §10, plan G1): the reviewed handshake +# framework rather than a hand-rolled key exchange. ring supplies AES-GCM + +# SHA-256, curve25519-dalek the X25519 DH (the same crate the HPKE path +# uses). Archive 599b506c…d82 matches crates.io (2025-07-19, mcginty); +# build script only detects the nightly channel. The RustCrypto sha2 / +# chacha20poly1305 features only satisfy snow's compile-time resolver +# check; the fallback resolver takes ring's SHA-256 and AES-GCM first. +snow = { version = "=0.10.0", default-features = false, features = ["ring-accelerated", "use-curve25519", "use-getrandom", "use-sha2", "use-chacha20poly1305"] } + +[dev-dependencies] +serde.workspace = true +serde_json.workspace = true diff --git a/crates/crypto/src/channel.rs b/crates/crypto/src/channel.rs new file mode 100644 index 000000000..ca5f2f2c1 --- /dev/null +++ b/crates/crypto/src/channel.rs @@ -0,0 +1,541 @@ +//! Authenticated device channel (RFC 0001 §10; plan G1): one fixed Noise +//! profile, `Noise_XX_25519_AESGCM_SHA256`, run over the untrusted device +//! relay. Both peers authenticate with their vault X25519 identity (the same +//! static key membership publishes as each device's encryption key) and are +//! ACCEPTED only when the caller's membership lookup says that static key +//! belongs to an active member whose device id matches the id sent inside +//! the handshake. The relay's connection ids are routing, never identity. +//! +//! The prologue binds the vault and storage generation so a transcript from +//! another vault cannot be replayed here. After the handshake the +//! [`Channel`] wraps application bytes with directional keys, Noise's +//! per-direction nonces, chunking for messages beyond Noise's 64 KiB limit, +//! and an explicit message budget after which the session must be +//! re-established (no unbounded key use). +//! +//! What this does NOT decide: which RPC methods a peer may call, or that the +//! peer is the device the relay was dialed by — every approved member is a +//! full-trust peer in v1 (RFC D3), so mutual membership is the authorization. + +use crate::CryptoError; +use std::fmt; +use zeroize::Zeroizing; + +const PATTERN: &str = "Noise_XX_25519_AESGCM_SHA256"; +const PROLOGUE_DOMAIN: &[u8] = b"zeron/device-channel/v1\0"; +/// Noise's hard cap per message; chunks stay below it. +const NOISE_MAX: usize = 65535; +const TAG_LEN: usize = 16; +/// Plaintext bytes per chunk: Noise max minus the tag minus the 1-byte +/// continuation flag. +const CHUNK_PLAINTEXT: usize = NOISE_MAX - TAG_LEN - 1; +/// Messages per direction before the session is retired (RFC §10 "explicit +/// rekey limits"): far below Noise's nonce space, comfortably above any +/// realistic remote-control session. +pub const MAX_MESSAGES_PER_DIRECTION: u64 = 1 << 32; +/// Application frame cap (a large file chunk is ~1 MiB base64). +pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ChannelError { + Crypto(CryptoError), + /// The Noise library refused the input (malformed, wrong order, or + /// authentication failure of a handshake/transport message). + Handshake, + /// The peer's static key or device id is not an active member. + PeerRejected, + /// A handshake message arrived out of sequence. + WrongState, + /// Frame or chunk limits exceeded. + SizeLimitExceeded, + /// The per-direction message budget is spent; reconnect. + Exhausted, + /// Chunked frame reassembly failed (truncated / trailing bytes). + Malformed, +} + +impl fmt::Display for ChannelError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for ChannelError {} +impl From for ChannelError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} +impl From for ChannelError { + fn from(_: snow::Error) -> Self { + Self::Handshake + } +} + +/// This device's channel identity: its vault device id and X25519 static. +pub struct ChannelIdentity { + device_id: [u8; 16], + static_key: Zeroizing<[u8; 32]>, +} + +impl ChannelIdentity { + pub fn new(device_id: [u8; 16], static_key: &[u8]) -> Result { + let key: [u8; 32] = static_key + .try_into() + .map_err(|_| ChannelError::Crypto(CryptoError::InvalidKeyLength))?; + Ok(Self { + device_id, + static_key: Zeroizing::new(key), + }) + } + + pub fn device_id(&self) -> &[u8; 16] { + &self.device_id + } + + /// The X25519 public key membership lists for this device. + pub fn public_key(&self) -> [u8; 32] { + crate::hpke::HpkePrivateKey::from_bytes(self.static_key.as_ref()) + .map(|key| *key.public_key().as_bytes()) + .unwrap_or([0; 32]) + } +} + +impl fmt::Debug for ChannelIdentity { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ChannelIdentity([REDACTED])") + } +} + +/// The vault scope both peers must share (prologue material). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ChannelScope { + pub vault_id: [u8; 16], + pub generation: [u8; 16], +} + +impl ChannelScope { + fn prologue(&self) -> Vec { + let mut out = Vec::with_capacity(PROLOGUE_DOMAIN.len() + 32); + out.extend_from_slice(PROLOGUE_DOMAIN); + out.extend_from_slice(&self.vault_id); + out.extend_from_slice(&self.generation); + out + } +} + +/// Which side of the handshake this device plays. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Role { + Initiator, + Responder, +} + +/// The peer this side authenticated: its device id (from the encrypted +/// handshake payload) and static key (from the Noise transcript). The +/// caller MUST check both against verified membership before trusting the +/// channel (see [`Handshake::finish`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PeerIdentity { + pub device_id: [u8; 16], + pub static_key: [u8; 32], +} + +/// An in-progress Noise XX handshake. +pub struct Handshake { + state: snow::HandshakeState, + role: Role, + local_device_id: [u8; 16], + peer: Option, + step: u8, +} + +impl fmt::Debug for Handshake { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "Handshake({:?}, step {})", self.role, self.step) + } +} + +fn build( + identity: &ChannelIdentity, + scope: &ChannelScope, + role: Role, +) -> Result { + let params: snow::params::NoiseParams = PATTERN.parse().map_err(|_| ChannelError::Handshake)?; + // snow borrows the prologue and private key only until `build_*` + // consumes the builder, so both live on this frame. + let prologue = scope.prologue(); + let builder = snow::Builder::new(params) + .local_private_key(identity.static_key.as_ref())? + .prologue(&prologue)?; + Ok(match role { + Role::Initiator => builder.build_initiator()?, + Role::Responder => builder.build_responder()?, + }) +} + +impl Handshake { + /// Start as the initiator; returns the first message (`-> e`). + pub fn initiate( + identity: &ChannelIdentity, + scope: &ChannelScope, + ) -> Result<(Self, Vec), ChannelError> { + let mut state = build(identity, scope, Role::Initiator)?; + let mut buffer = vec![0u8; NOISE_MAX]; + let length = state.write_message(&[], &mut buffer)?; + buffer.truncate(length); + Ok(( + Self { + state, + role: Role::Initiator, + local_device_id: identity.device_id, + peer: None, + step: 1, + }, + buffer, + )) + } + + /// Start as the responder with the initiator's first message; returns + /// the second message (`<- e, ee, s, es` carrying our device id). + pub fn respond( + identity: &ChannelIdentity, + scope: &ChannelScope, + first: &[u8], + ) -> Result<(Self, Vec), ChannelError> { + if first.len() > NOISE_MAX { + return Err(ChannelError::SizeLimitExceeded); + } + let mut state = build(identity, scope, Role::Responder)?; + let mut payload = vec![0u8; NOISE_MAX]; + let read = state.read_message(first, &mut payload)?; + if read != 0 { + return Err(ChannelError::Handshake); + } + let mut buffer = vec![0u8; NOISE_MAX]; + let length = state.write_message(&identity.device_id, &mut buffer)?; + buffer.truncate(length); + Ok(( + Self { + state, + role: Role::Responder, + local_device_id: identity.device_id, + peer: None, + step: 2, + }, + buffer, + )) + } + + /// Initiator: consume the responder's message; returns the third + /// message (`-> s, se` carrying our device id) and the responder's + /// identity for the membership check. + pub fn initiator_step( + &mut self, + second: &[u8], + ) -> Result<(Vec, PeerIdentity), ChannelError> { + if self.role != Role::Initiator || self.step != 1 { + return Err(ChannelError::WrongState); + } + if second.len() > NOISE_MAX { + return Err(ChannelError::SizeLimitExceeded); + } + let mut payload = vec![0u8; NOISE_MAX]; + let read = self.state.read_message(second, &mut payload)?; + let device_id: [u8; 16] = payload[..read] + .try_into() + .map_err(|_| ChannelError::Handshake)?; + let static_key: [u8; 32] = self + .state + .get_remote_static() + .and_then(|key| key.try_into().ok()) + .ok_or(ChannelError::Handshake)?; + let mut buffer = vec![0u8; NOISE_MAX]; + let length = self + .state + .write_message(&self.local_device_id, &mut buffer)?; + buffer.truncate(length); + let peer = PeerIdentity { + device_id, + static_key, + }; + self.peer = Some(peer); + self.step = 3; + Ok((buffer, peer)) + } + + /// Responder: consume the initiator's third message; returns the + /// initiator's identity for the membership check. + pub fn responder_step(&mut self, third: &[u8]) -> Result { + if self.role != Role::Responder || self.step != 2 { + return Err(ChannelError::WrongState); + } + if third.len() > NOISE_MAX { + return Err(ChannelError::SizeLimitExceeded); + } + let mut payload = vec![0u8; NOISE_MAX]; + let read = self.state.read_message(third, &mut payload)?; + let device_id: [u8; 16] = payload[..read] + .try_into() + .map_err(|_| ChannelError::Handshake)?; + let static_key: [u8; 32] = self + .state + .get_remote_static() + .and_then(|key| key.try_into().ok()) + .ok_or(ChannelError::Handshake)?; + let peer = PeerIdentity { + device_id, + static_key, + }; + self.peer = Some(peer); + self.step = 3; + Ok(peer) + } + + /// The authenticated peer (once the transcript has revealed it). + pub fn peer(&self) -> Option { + self.peer + } + + /// Complete the handshake. `accept` is the caller's membership check on + /// the peer identity — return `false` for anything but an ACTIVE member + /// whose published encryption key equals `static_key` and whose device + /// id equals `device_id`. A rejected peer yields no channel. + pub fn finish( + self, + accept: impl FnOnce(&PeerIdentity) -> bool, + ) -> Result { + if !self.state.is_handshake_finished() || self.step != 3 { + return Err(ChannelError::WrongState); + } + let peer = self.peer.ok_or(ChannelError::WrongState)?; + if !accept(&peer) { + return Err(ChannelError::PeerRejected); + } + let transport = self.state.into_transport_mode()?; + Ok(Channel { + transport, + peer, + sent: 0, + received: 0, + }) + } +} + +/// An established channel: encrypt/decrypt application frames. +pub struct Channel { + transport: snow::TransportState, + peer: PeerIdentity, + sent: u64, + received: u64, +} + +impl fmt::Debug for Channel { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "Channel(sent {}, received {})", + self.sent, self.received + ) + } +} + +impl Channel { + pub fn peer(&self) -> &PeerIdentity { + &self.peer + } + + pub fn sent(&self) -> u64 { + self.sent + } + + pub fn received(&self) -> u64 { + self.received + } + + /// Seal one application frame. Output: a sequence of Noise messages, + /// each `u16 BE length || ciphertext`; the first plaintext byte of every + /// chunk is a continuation flag (1 = more chunks follow). + pub fn seal(&mut self, frame: &[u8]) -> Result, ChannelError> { + if frame.len() > MAX_FRAME_BYTES { + return Err(ChannelError::SizeLimitExceeded); + } + let chunks = frame.chunks(CHUNK_PLAINTEXT).count().max(1) as u64; + if self.sent.saturating_add(chunks) > MAX_MESSAGES_PER_DIRECTION { + return Err(ChannelError::Exhausted); + } + let mut out = Vec::with_capacity(frame.len() + 64); + let mut plaintext = Zeroizing::new(vec![0u8; NOISE_MAX]); + let mut buffer = vec![0u8; NOISE_MAX]; + let mut pieces: Vec<&[u8]> = frame.chunks(CHUNK_PLAINTEXT).collect(); + if pieces.is_empty() { + pieces.push(&[]); + } + let last = pieces.len() - 1; + for (index, piece) in pieces.iter().enumerate() { + plaintext[0] = u8::from(index != last); + plaintext[1..1 + piece.len()].copy_from_slice(piece); + let length = self + .transport + .write_message(&plaintext[..1 + piece.len()], &mut buffer)?; + self.sent += 1; + out.extend_from_slice(&(length as u16).to_be_bytes()); + out.extend_from_slice(&buffer[..length]); + } + Ok(out) + } + + /// Open one sealed frame produced by the peer's [`Self::seal`]. + pub fn open(&mut self, sealed: &[u8]) -> Result, ChannelError> { + if sealed.len() > MAX_FRAME_BYTES + (MAX_FRAME_BYTES / CHUNK_PLAINTEXT + 1) * (TAG_LEN + 3) + { + return Err(ChannelError::SizeLimitExceeded); + } + let mut out = Vec::new(); + let mut cursor = 0usize; + let mut buffer = vec![0u8; NOISE_MAX]; + loop { + if sealed.len() < cursor + 2 { + return Err(ChannelError::Malformed); + } + let length = u16::from_be_bytes([sealed[cursor], sealed[cursor + 1]]) as usize; + cursor += 2; + if length < TAG_LEN + 1 || sealed.len() < cursor + length { + return Err(ChannelError::Malformed); + } + if self.received >= MAX_MESSAGES_PER_DIRECTION { + return Err(ChannelError::Exhausted); + } + let read = self + .transport + .read_message(&sealed[cursor..cursor + length], &mut buffer)?; + self.received += 1; + cursor += length; + if read == 0 { + return Err(ChannelError::Malformed); + } + out.extend_from_slice(&buffer[1..read]); + if out.len() > MAX_FRAME_BYTES { + return Err(ChannelError::SizeLimitExceeded); + } + match buffer[0] { + 0 => break, + 1 => continue, + _ => return Err(ChannelError::Malformed), + } + } + if cursor != sealed.len() { + return Err(ChannelError::Malformed); + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn identity(tag: u8) -> ChannelIdentity { + ChannelIdentity::new([tag; 16], &[tag ^ 0x5a; 32]).unwrap() + } + + fn scope() -> ChannelScope { + ChannelScope { + vault_id: [1; 16], + generation: [2; 16], + } + } + + fn establish( + initiator: &ChannelIdentity, + responder: &ChannelIdentity, + scope_a: &ChannelScope, + scope_b: &ChannelScope, + ) -> Result<(Channel, Channel), ChannelError> { + let (mut a, m1) = Handshake::initiate(initiator, scope_a)?; + let (mut b, m2) = Handshake::respond(responder, scope_b, &m1)?; + let (m3, responder_seen) = a.initiator_step(&m2)?; + assert_eq!(responder_seen.device_id, *responder.device_id()); + assert_eq!(responder_seen.static_key, responder.public_key()); + let initiator_seen = b.responder_step(&m3)?; + assert_eq!(initiator_seen.device_id, *initiator.device_id()); + assert_eq!(initiator_seen.static_key, initiator.public_key()); + let responder_public = responder.public_key(); + let initiator_public = initiator.public_key(); + let channel_a = a.finish(|peer| peer.static_key == responder_public)?; + let channel_b = b.finish(|peer| peer.static_key == initiator_public)?; + Ok((channel_a, channel_b)) + } + + #[test] + fn handshake_authenticates_both_statics_and_carries_frames_both_ways() { + let (mut a, mut b) = + establish(&identity(0x11), &identity(0x22), &scope(), &scope()).unwrap(); + let sealed = a.seal(b"hello from the phone").unwrap(); + assert!(!sealed.windows(5).any(|w| w == b"hello")); + assert_eq!(b.open(&sealed).unwrap(), b"hello from the phone"); + let reply = b.seal(b"").unwrap(); + assert_eq!(a.open(&reply).unwrap(), b""); + // Replay and tampering are rejected; the channel stays usable. + assert!(b.open(&sealed).is_err()); + let mut damaged = a.seal(b"x").unwrap(); + let last = damaged.len() - 1; + damaged[last] ^= 1; + assert!(b.open(&damaged).is_err()); + assert_eq!(a.sent(), 2); + assert_eq!( + b.received(), + 1, + "a rejected frame is not counted as received" + ); + } + + #[test] + fn large_frames_chunk_and_reassemble_in_order() { + let (mut a, mut b) = + establish(&identity(0x11), &identity(0x22), &scope(), &scope()).unwrap(); + let big: Vec = (0..(3 * NOISE_MAX + 17)).map(|i| (i % 251) as u8).collect(); + let sealed = a.seal(&big).unwrap(); + assert!(sealed.len() > big.len()); + assert_eq!(b.open(&sealed).unwrap(), big); + // Truncated / reordered chunk streams fail closed. + assert!(b.open(&sealed[..sealed.len() - 5]).is_err()); + assert!(matches!( + a.seal(&vec![0u8; MAX_FRAME_BYTES + 1]), + Err(ChannelError::SizeLimitExceeded) + )); + } + + #[test] + fn membership_check_and_prologue_gate_the_channel() { + let initiator = identity(0x11); + let responder = identity(0x22); + // A responder the initiator's membership does not list: no channel. + let (mut a, m1) = Handshake::initiate(&initiator, &scope()).unwrap(); + let (mut b, m2) = Handshake::respond(&responder, &scope(), &m1).unwrap(); + let (m3, _) = a.initiator_step(&m2).unwrap(); + b.responder_step(&m3).unwrap(); + assert!(matches!( + a.finish(|_| false), + Err(ChannelError::PeerRejected) + )); + assert!(b.finish(|_| true).is_ok()); + // Different vault scope (prologue) breaks the transcript. + let other = ChannelScope { + vault_id: [9; 16], + generation: [2; 16], + }; + assert!(establish(&identity(0x11), &identity(0x22), &scope(), &other).is_err()); + // Out-of-order steps are refused. + let (mut a, _) = Handshake::initiate(&initiator, &scope()).unwrap(); + assert!(matches!( + a.responder_step(&[0; 48]), + Err(ChannelError::WrongState) + )); + assert!(matches!( + a.initiator_step(&[0; 10]), + Err(ChannelError::Handshake) + )); + assert_eq!(format!("{initiator:?}"), "ChannelIdentity([REDACTED])"); + } +} diff --git a/crates/crypto/src/content.rs b/crates/crypto/src/content.rs new file mode 100644 index 000000000..6a146df5d --- /dev/null +++ b/crates/crypto/src/content.rs @@ -0,0 +1,437 @@ +use crate::record::{self, Reader, RecordBinding, RecordError, RecordKind, UnverifiedRecord}; +use crate::{CryptoError, SecretBytes, hkdf_sha256, open_aes256_gcm}; +use ring::{aead, rand::SecureRandom, signature::KeyPair}; +use std::fmt; +use zeroize::Zeroizing; + +pub const MAX_PLAINTEXT_BYTES: usize = 16 * 1024 * 1024 - 400; +const PAYLOAD_OVERHEAD: usize = 144; +const KEY_DOMAIN: &[u8] = b"zeron/content/key/v1\0"; +const AAD_DOMAIN: &[u8] = b"zeron/content/aad/v1\0"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ContentError { + Record(RecordError), + Crypto(CryptoError), + InvalidKey, + InvalidSigningKey, + WrongScope, + WrongAuthor, + WrongKind, + WrongKey, + WrongPurpose, + UnsupportedFormat, + UnsupportedSuite, + UnsupportedPurpose, + SizeLimitExceeded, + EntropyUnavailable, +} + +impl fmt::Display for ContentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for ContentError {} +impl From for ContentError { + fn from(error: RecordError) -> Self { + Self::Record(error) + } +} +impl From for ContentError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum ContentPurpose { + ChatUpdate = 1, + Checkpoint = 2, + Frontier = 3, + RegistryField = 4, + Tail = 5, + Diff = 6, + Blob = 7, + DeviceSidecar = 8, + /// A registry row lifecycle proof (RFC 0001 §9): the authenticated + /// plaintext names the row and its tombstone clock. + RegistryLifecycle = 9, +} + +impl TryFrom for ContentPurpose { + type Error = ContentError; + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(Self::ChatUpdate), + 2 => Ok(Self::Checkpoint), + 3 => Ok(Self::Frontier), + 4 => Ok(Self::RegistryField), + 5 => Ok(Self::Tail), + 6 => Ok(Self::Diff), + 7 => Ok(Self::Blob), + 8 => Ok(Self::DeviceSidecar), + 9 => Ok(Self::RegistryLifecycle), + _ => Err(ContentError::UnsupportedPurpose), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct KeyScope { + pub vault_id: [u8; 16], + pub generation: [u8; 16], + pub epoch: u64, + pub object_id: [u8; 16], +} + +impl From<&RecordBinding> for KeyScope { + fn from(binding: &RecordBinding) -> Self { + Self { + vault_id: binding.vault_id, + generation: binding.generation, + epoch: binding.epoch, + object_id: binding.object_id, + } + } +} + +pub struct ContentKey { + scope: KeyScope, + identifier: [u8; 16], + bytes: Zeroizing<[u8; 32]>, +} + +impl ContentKey { + pub fn from_bytes( + scope: KeyScope, + identifier: [u8; 16], + bytes: &[u8], + ) -> Result { + if scope.epoch == 0 { + return Err(ContentError::WrongScope); + } + let bytes = bytes.try_into().map_err(|_| ContentError::InvalidKey)?; + Ok(Self { + scope, + identifier, + bytes: Zeroizing::new(bytes), + }) + } + + pub fn generate(scope: KeyScope) -> Result { + if scope.epoch == 0 { + return Err(ContentError::WrongScope); + } + let mut material = Zeroizing::new([0; 48]); + ring::rand::SystemRandom::new() + .fill(material.as_mut()) + .map_err(|_| ContentError::EntropyUnavailable)?; + let identifier = material[..16] + .try_into() + .map_err(|_| ContentError::InvalidKey)?; + Self::from_bytes(scope, identifier, &material[16..]) + } + + pub fn scope(&self) -> KeyScope { + self.scope + } + pub fn identifier(&self) -> &[u8; 16] { + &self.identifier + } + pub fn expose_secret(&self) -> &[u8; 32] { + &self.bytes + } +} + +impl fmt::Debug for ContentKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ContentKey([REDACTED])") + } +} + +pub struct DeviceSigner { + author_id: [u8; 16], + key_pair: ring::signature::Ed25519KeyPair, +} + +impl DeviceSigner { + pub fn from_seed(author_id: [u8; 16], seed: &[u8]) -> Result { + if seed.len() != 32 { + return Err(ContentError::InvalidSigningKey); + } + let key_pair = ring::signature::Ed25519KeyPair::from_seed_unchecked(seed) + .map_err(|_| ContentError::InvalidSigningKey)?; + if !crate::ed25519_point_encoding_precheck(key_pair.public_key().as_ref()) { + return Err(ContentError::InvalidSigningKey); + } + Ok(Self { + author_id, + key_pair, + }) + } + + pub fn author_id(&self) -> &[u8; 16] { + &self.author_id + } + pub fn public_key(&self) -> &[u8] { + self.key_pair.public_key().as_ref() + } + + /// Ordinary Ed25519 over `message` (callers supply the domain-separated + /// signed-record input; this is never exposed as a raw RPC). + pub(crate) fn sign_bytes(&self, message: &[u8]) -> Result<[u8; 64], ContentError> { + self.key_pair + .sign(message) + .as_ref() + .try_into() + .map_err(|_| ContentError::InvalidSigningKey) + } +} + +impl fmt::Debug for DeviceSigner { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("DeviceSigner([REDACTED])") + } +} + +pub struct SealedContent { + binding: RecordBinding, + purpose: ContentPurpose, + revision_id: [u8; 16], + encoded: Vec, +} + +impl SealedContent { + pub fn purpose(&self) -> ContentPurpose { + self.purpose + } + pub fn binding(&self) -> &RecordBinding { + &self.binding + } + pub fn revision_id(&self) -> &[u8; 16] { + &self.revision_id + } + pub fn encoded(&self) -> &[u8] { + &self.encoded + } +} + +impl fmt::Debug for SealedContent { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("SealedContent([REDACTED])") + } +} + +pub struct OpenedContent { + revision_id: [u8; 16], + plaintext: SecretBytes, +} + +impl OpenedContent { + pub fn revision_id(&self) -> &[u8; 16] { + &self.revision_id + } + pub fn plaintext(&self) -> &SecretBytes { + &self.plaintext + } +} + +impl fmt::Debug for OpenedContent { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("OpenedContent([REDACTED])") + } +} + +pub fn seal( + binding: &RecordBinding, + purpose: ContentPurpose, + key: &ContentKey, + signer: &DeviceSigner, + plaintext: &[u8], + max_plaintext_bytes: usize, +) -> Result { + seal_with_random( + binding, + purpose, + key, + signer, + plaintext, + max_plaintext_bytes, + |material| { + ring::rand::SystemRandom::new() + .fill(material) + .map_err(|_| ContentError::EntropyUnavailable) + }, + ) +} + +fn seal_with_random( + binding: &RecordBinding, + purpose: ContentPurpose, + key: &ContentKey, + signer: &DeviceSigner, + plaintext: &[u8], + max_plaintext_bytes: usize, + random: impl FnOnce(&mut [u8; 48]) -> Result<(), ContentError>, +) -> Result { + let payload_limit = payload_limit(max_plaintext_bytes)?; + check_scope(binding, key)?; + if binding.author_id != signer.author_id { + return Err(ContentError::WrongAuthor); + } + if plaintext.len() > max_plaintext_bytes { + return Err(ContentError::SizeLimitExceeded); + } + let mut material = [0; 48]; + random(&mut material)?; + let revision_id: [u8; 16] = material[..16] + .try_into() + .map_err(|_| ContentError::EntropyUnavailable)?; + let salt: [u8; 32] = material[16..] + .try_into() + .map_err(|_| ContentError::EntropyUnavailable)?; + let header = protected_header(5, purpose, &key.identifier, &salt); + let context = record::context_bytes(binding, &revision_id)?; + let derived = derive_key(key, &salt, &context, &header)?; + let aad = contextual_bytes(AAD_DOMAIN, &context, &header); + let encryption_key = aead::LessSafeKey::new( + aead::UnboundKey::new(&aead::AES_256_GCM, derived.as_bytes()) + .map_err(|_| ContentError::InvalidKey)?, + ); + let mut ciphertext = Zeroizing::new(Vec::with_capacity(plaintext.len() + 16)); + ciphertext.extend_from_slice(plaintext); + encryption_key + .seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key([0; 12]), + aead::Aad::from(&aad), + &mut *ciphertext, + ) + .map_err(|_| ContentError::Crypto(CryptoError::AuthenticationFailed))?; + let mut payload = protected_header(6, purpose, &key.identifier, &salt); + record::bytes_field(&mut payload, 5, &ciphertext); + let signature_input = record::signing_bytes(binding, &revision_id, &payload, payload_limit)?; + let signature = signer.sign_bytes(&signature_input)?; + let encoded = + record::encode_signed(binding, &revision_id, &payload, &signature, payload_limit)?; + Ok(SealedContent { + binding: *binding, + purpose, + revision_id, + encoded, + }) +} + +pub fn open( + encoded: &[u8], + expected: &RecordBinding, + purpose: ContentPurpose, + key: &ContentKey, + trusted_public_key: &[u8], + max_plaintext_bytes: usize, +) -> Result { + let payload_limit = payload_limit(max_plaintext_bytes)?; + check_scope(expected, key)?; + let record = + UnverifiedRecord::parse(encoded, payload_limit)?.verify(expected, trusted_public_key)?; + let mut reader = Reader::new(record.payload()); + if reader.argument(5)? != 6 { + return Err(ContentError::UnsupportedFormat); + } + if reader.uint_field(0)? != 1 { + return Err(ContentError::UnsupportedFormat); + } + if reader.uint_field(1)? != 1 { + return Err(ContentError::UnsupportedSuite); + } + let stored_purpose = ContentPurpose::try_from(reader.uint_field(2)?)?; + if stored_purpose != purpose { + return Err(ContentError::WrongPurpose); + } + let identifier: [u8; 16] = reader.fixed_field(3)?; + if identifier != key.identifier { + return Err(ContentError::WrongKey); + } + let salt: [u8; 32] = reader.fixed_field(4)?; + let ciphertext = reader.bytes_field(5, max_plaintext_bytes + 16)?; + reader.finish()?; + if ciphertext.len() < 16 { + return Err(ContentError::UnsupportedFormat); + } + let header = protected_header(5, purpose, &identifier, &salt); + let context = record::context_bytes(expected, record.revision_id())?; + let derived = derive_key(key, &salt, &context, &header)?; + let aad = contextual_bytes(AAD_DOMAIN, &context, &header); + let plaintext = open_aes256_gcm( + derived.as_bytes(), + &[0; 12], + &aad, + ciphertext, + max_plaintext_bytes, + )?; + Ok(OpenedContent { + revision_id: *record.revision_id(), + plaintext, + }) +} + +fn check_scope(binding: &RecordBinding, key: &ContentKey) -> Result<(), ContentError> { + if binding.kind != RecordKind::Content { + return Err(ContentError::WrongKind); + } + if key.scope != KeyScope::from(binding) { + return Err(ContentError::WrongScope); + } + Ok(()) +} + +fn payload_limit(max_plaintext_bytes: usize) -> Result { + if max_plaintext_bytes > MAX_PLAINTEXT_BYTES { + return Err(ContentError::SizeLimitExceeded); + } + Ok(max_plaintext_bytes + PAYLOAD_OVERHEAD) +} + +fn protected_header( + count: u64, + purpose: ContentPurpose, + identifier: &[u8; 16], + salt: &[u8; 32], +) -> Vec { + let mut header = Vec::with_capacity(128); + record::argument(&mut header, 5, count); + record::uint_field(&mut header, 0, 1); + record::uint_field(&mut header, 1, 1); + record::uint_field(&mut header, 2, purpose as u64); + record::bytes_field(&mut header, 3, identifier); + record::bytes_field(&mut header, 4, salt); + header +} + +fn contextual_bytes(domain: &[u8], context: &[u8], header: &[u8]) -> Vec { + let mut bytes = Vec::with_capacity(domain.len() + context.len() + header.len()); + bytes.extend_from_slice(domain); + bytes.extend_from_slice(context); + bytes.extend_from_slice(header); + bytes +} + +fn derive_key( + key: &ContentKey, + salt: &[u8; 32], + context: &[u8], + header: &[u8], +) -> Result { + Ok(hkdf_sha256( + key.expose_secret(), + salt, + &contextual_bytes(KEY_DOMAIN, context, header), + 32, + )?) +} + +#[cfg(test)] +#[path = "content_tests.rs"] +mod tests; diff --git a/crates/crypto/src/content_tests.rs b/crates/crypto/src/content_tests.rs new file mode 100644 index 000000000..674e487fc --- /dev/null +++ b/crates/crypto/src/content_tests.rs @@ -0,0 +1,451 @@ +use super::*; +use crate::record::{RecordBinding, RecordKind}; + +type TestResult = Result<(), Box>; + +fn binding() -> RecordBinding { + RecordBinding { + kind: RecordKind::Content, + vault_id: [1; 16], + generation: [2; 16], + epoch: 1, + object_id: [3; 16], + author_id: [4; 16], + membership_hash: [5; 32], + } +} + +fn keys() -> Result<(ContentKey, DeviceSigner), ContentError> { + Ok(( + ContentKey::from_bytes(KeyScope::from(&binding()), [6; 16], &[7; 32])?, + DeviceSigner::from_seed([4; 16], &[8; 32])?, + )) +} + +#[test] +fn content_round_trip_and_immutable_retry_bytes() -> TestResult { + let (key, signer) = keys()?; + let plaintext = b"private transcript\0\xff"; + let sealed = seal( + &binding(), + ContentPurpose::ChatUpdate, + &key, + &signer, + plaintext, + 1024, + )?; + let retry = sealed.encoded().to_vec(); + let opened = open( + sealed.encoded(), + &binding(), + ContentPurpose::ChatUpdate, + &key, + signer.public_key(), + 1024, + )?; + assert_eq!(opened.plaintext().as_bytes(), plaintext); + assert_eq!(opened.revision_id(), sealed.revision_id()); + assert_eq!(retry, sealed.encoded()); + assert!( + !sealed + .encoded() + .windows(plaintext.len()) + .any(|window| window == plaintext) + ); + assert_eq!(format!("{key:?}"), "ContentKey([REDACTED])"); + assert_eq!(format!("{signer:?}"), "DeviceSigner([REDACTED])"); + assert_eq!(format!("{opened:?}"), "OpenedContent([REDACTED])"); + Ok(()) +} + +#[test] +fn content_fresh_seals_are_distinct_and_wrong_keys_fail() -> TestResult { + let (key, signer) = keys()?; + let first = seal( + &binding(), + ContentPurpose::Tail, + &key, + &signer, + b"same content", + 1024, + )?; + let second = seal( + &binding(), + ContentPurpose::Tail, + &key, + &signer, + b"same content", + 1024, + )?; + assert_ne!(first.revision_id(), second.revision_id()); + assert_ne!(first.encoded(), second.encoded()); + let wrong_key = ContentKey::from_bytes(KeyScope::from(&binding()), [6; 16], &[9; 32])?; + assert!( + open( + first.encoded(), + &binding(), + ContentPurpose::Tail, + &wrong_key, + signer.public_key(), + 1024 + ) + .is_err() + ); + let wrong_id = ContentKey::from_bytes(KeyScope::from(&binding()), [9; 16], &[7; 32])?; + assert_eq!( + open( + first.encoded(), + &binding(), + ContentPurpose::Tail, + &wrong_id, + signer.public_key(), + 1024 + ) + .err(), + Some(ContentError::WrongKey) + ); + assert_eq!( + open( + first.encoded(), + &binding(), + ContentPurpose::Blob, + &key, + signer.public_key(), + 1024 + ) + .err(), + Some(ContentError::WrongPurpose) + ); + Ok(()) +} + +#[test] +fn content_rejects_tampering_and_truncation() -> TestResult { + let (key, signer) = keys()?; + let sealed = seal( + &binding(), + ContentPurpose::ChatUpdate, + &key, + &signer, + b"canary", + 128, + )?; + for index in 0..sealed.encoded().len() { + let mut changed = sealed.encoded().to_vec(); + if let Some(byte) = changed.get_mut(index) { + *byte ^= 1; + } + assert!( + open( + &changed, + &binding(), + ContentPurpose::ChatUpdate, + &key, + signer.public_key(), + 128 + ) + .is_err() + ); + } + for length in 0..sealed.encoded().len() { + let truncated = sealed.encoded().get(..length).ok_or("invalid test range")?; + assert!( + open( + truncated, + &binding(), + ContentPurpose::ChatUpdate, + &key, + signer.public_key(), + 128 + ) + .is_err() + ); + } + Ok(()) +} + +#[test] +fn content_authenticates_ciphertext_and_context_even_after_resigning() -> TestResult { + let (key, signer) = keys()?; + let sealed = seal( + &binding(), + ContentPurpose::ChatUpdate, + &key, + &signer, + b"private", + 128, + )?; + let verified = UnverifiedRecord::parse(sealed.encoded(), payload_limit(128)?)? + .verify(&binding(), signer.public_key())?; + let mut damaged = verified.payload().to_vec(); + *damaged.last_mut().ok_or("empty encrypted payload")? ^= 1; + let input = record::signing_bytes( + &binding(), + sealed.revision_id(), + &damaged, + payload_limit(128)?, + )?; + let signature: [u8; 64] = signer.key_pair.sign(&input).as_ref().try_into()?; + let encoded = record::encode_signed( + &binding(), + sealed.revision_id(), + &damaged, + &signature, + payload_limit(128)?, + )?; + assert_eq!( + open( + &encoded, + &binding(), + ContentPurpose::ChatUpdate, + &key, + signer.public_key(), + 128 + ) + .err(), + Some(ContentError::Crypto(CryptoError::AuthenticationFailed)) + ); + let mut changed_context = binding(); + changed_context.membership_hash = [9; 32]; + let input = record::signing_bytes( + &changed_context, + sealed.revision_id(), + verified.payload(), + payload_limit(128)?, + )?; + let signature: [u8; 64] = signer.key_pair.sign(&input).as_ref().try_into()?; + let encoded = record::encode_signed( + &changed_context, + sealed.revision_id(), + verified.payload(), + &signature, + payload_limit(128)?, + )?; + assert!( + open( + &encoded, + &changed_context, + ContentPurpose::ChatUpdate, + &key, + signer.public_key(), + 128 + ) + .is_err() + ); + Ok(()) +} + +#[test] +fn content_key_generation_and_empty_payloads() -> TestResult { + let (key, signer) = keys()?; + let generated = ContentKey::generate(KeyScope::from(&binding()))?; + let another = ContentKey::generate(KeyScope::from(&binding()))?; + assert_ne!(generated.identifier(), another.identifier()); + assert_ne!(generated.expose_secret(), another.expose_secret()); + for purpose in 1..=8 { + let purpose = ContentPurpose::try_from(purpose)?; + let sealed = seal(&binding(), purpose, &key, &signer, &[], 0)?; + assert!( + open( + sealed.encoded(), + &binding(), + purpose, + &key, + signer.public_key(), + 0 + )? + .plaintext() + .as_bytes() + .is_empty() + ); + } + assert!(ContentKey::from_bytes(KeyScope::from(&binding()), [6; 16], &[7; 31]).is_err()); + Ok(()) +} + +#[test] +fn content_shared_fixture_and_swift_records() -> TestResult { + use crate::tests::hex; + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct Fixtures { + encrypted_content: Vec, + } + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct Fixture { + name: String, + vault_id: String, + generation: String, + epoch: u64, + object_id: String, + author_id: String, + membership_hash: String, + purpose: u64, + key_id: String, + content_key: String, + signer_seed: String, + public_key: String, + plaintext: String, + encoded: String, + peer_record: Option, + } + let live = std::env::var("ZERON_CRYPTO_TEST_VECTORS") + .ok() + .map(std::fs::read_to_string) + .transpose()?; + let fixtures: Fixtures = serde_json::from_str( + live.as_deref() + .unwrap_or(include_str!("../tests/fixtures/primitives.json")), + )?; + assert!(!fixtures.encrypted_content.is_empty()); + for fixture in fixtures.encrypted_content { + let context = RecordBinding { + kind: RecordKind::Content, + vault_id: hex(&fixture.vault_id).as_slice().try_into()?, + generation: hex(&fixture.generation).as_slice().try_into()?, + epoch: fixture.epoch, + object_id: hex(&fixture.object_id).as_slice().try_into()?, + author_id: hex(&fixture.author_id).as_slice().try_into()?, + membership_hash: hex(&fixture.membership_hash).as_slice().try_into()?, + }; + let key = ContentKey::from_bytes( + KeyScope::from(&context), + hex(&fixture.key_id).as_slice().try_into()?, + &hex(&fixture.content_key), + )?; + let signer = DeviceSigner::from_seed(context.author_id, &hex(&fixture.signer_seed))?; + assert_eq!(signer.public_key(), hex(&fixture.public_key)); + let purpose = ContentPurpose::try_from(fixture.purpose)?; + let plaintext = hex(&fixture.plaintext); + let encoded = hex(&fixture.encoded); + let sealed = seal_with_random( + &context, + purpose, + &key, + &signer, + &plaintext, + plaintext.len(), + |material| { + for (byte, value) in material.iter_mut().zip(0u8..) { + *byte = value; + } + Ok(()) + }, + )?; + assert_eq!(sealed.encoded(), encoded, "{}", fixture.name); + assert_eq!( + open( + &encoded, + &context, + purpose, + &key, + signer.public_key(), + plaintext.len() + )? + .plaintext() + .as_bytes(), + plaintext + ); + if live.is_some() { + let peer_record = hex(fixture + .peer_record + .as_deref() + .ok_or("missing Swift encrypted record")?); + assert_eq!( + open( + &peer_record, + &context, + purpose, + &key, + signer.public_key(), + plaintext.len() + )? + .plaintext() + .as_bytes(), + plaintext + ); + } + } + Ok(()) +} + +#[test] +fn content_scope_and_author_are_checked_before_entropy() -> TestResult { + let (key, signer) = keys()?; + let mut entropy_calls = 0; + let mut changed = binding(); + changed.epoch = 2; + let result = seal_with_random( + &changed, + ContentPurpose::ChatUpdate, + &key, + &signer, + b"x", + 1, + |_| { + entropy_calls += 1; + Err(ContentError::EntropyUnavailable) + }, + ); + assert_eq!(result.err(), Some(ContentError::WrongScope)); + changed = binding(); + changed.author_id = [9; 16]; + let result = seal_with_random( + &changed, + ContentPurpose::ChatUpdate, + &key, + &signer, + b"x", + 1, + |_| { + entropy_calls += 1; + Err(ContentError::EntropyUnavailable) + }, + ); + assert_eq!(result.err(), Some(ContentError::WrongAuthor)); + assert_eq!(entropy_calls, 0); + Ok(()) +} + +#[test] +fn content_entropy_and_size_failures_do_not_produce_a_record() -> TestResult { + let (key, signer) = keys()?; + let result = seal_with_random( + &binding(), + ContentPurpose::ChatUpdate, + &key, + &signer, + b"x", + 1, + |_| Err(ContentError::EntropyUnavailable), + ); + assert_eq!(result.err(), Some(ContentError::EntropyUnavailable)); + let mut entropy_calls = 0; + let result = seal_with_random( + &binding(), + ContentPurpose::ChatUpdate, + &key, + &signer, + b"xx", + 1, + |_| { + entropy_calls += 1; + Err(ContentError::EntropyUnavailable) + }, + ); + assert_eq!(result.err(), Some(ContentError::SizeLimitExceeded)); + assert_eq!(entropy_calls, 0); + assert!( + seal( + &binding(), + ContentPurpose::ChatUpdate, + &key, + &signer, + b"x", + usize::MAX + ) + .is_err() + ); + Ok(()) +} diff --git a/crates/crypto/src/envelope.rs b/crates/crypto/src/envelope.rs new file mode 100644 index 000000000..8c3a26623 --- /dev/null +++ b/crates/crypto/src/envelope.rs @@ -0,0 +1,561 @@ +//! Key envelopes (RFC 0001 §5): signed-wrapper kind = KeyEnvelope records +//! that carry either +//! +//! * a **keyring envelope** — the workspace keyring HPKE-sealed to one +//! recipient (an approved device or the recovery authority), or +//! * an **object key envelope** — one object's random content root key +//! wrapped under the epoch key that created it (AES-256-GCM with a fresh +//! per-record derived key, the same construction as content records). +//! +//! Payload map: +//! +//! ```text +//! 0 envelope version unsigned, exactly 1 +//! 1 recipient kind 1 device, 2 recovery authority, 3 epoch key +//! 2 recipient id 16 bytes (device/authority id, or the epoch as a +//! big-endian u64 right-aligned in 16 bytes) +//! 3 encapsulation 32 bytes (HPKE enc, or the derivation salt) +//! 4 ciphertext + tag bounded byte string +//! ``` +//! +//! Fields 0..3 are the protected header H; C is the wrapper context +//! (`record::context_bytes`). HPKE info = "zeron/keyring-envelope/v1" || 0x00 +//! || C || H. Object keys derive K = HKDF(epoch key, salt, +//! "zeron/object-key/v1" || 0x00 || C || H) and AAD = "zeron/object-key/aad/v1" +//! || 0x00 || C || H, then encrypt id || key with a zero nonce exactly once. + +use crate::content::{ContentError, ContentKey, DeviceSigner, KeyScope}; +use crate::hpke::{self, HpkePrivateKey, HpkePublicKey}; +use crate::keyring::{Keyring, KeyringError, MAX_KEYRING_BYTES}; +use crate::record::{self, Reader, RecordBinding, RecordError, RecordKind, UnverifiedRecord}; +use crate::{CryptoError, hkdf_sha256, open_aes256_gcm}; +use ring::aead; +use std::fmt; +use zeroize::Zeroizing; + +const KEYRING_INFO_DOMAIN: &[u8] = b"zeron/keyring-envelope/v1\0"; +const OBJECT_KEY_DOMAIN: &[u8] = b"zeron/object-key/v1\0"; +const OBJECT_AAD_DOMAIN: &[u8] = b"zeron/object-key/aad/v1\0"; +const PAYLOAD_OVERHEAD: usize = 128; +pub const MAX_ENVELOPE_PAYLOAD_BYTES: usize = MAX_KEYRING_BYTES + 16 + PAYLOAD_OVERHEAD; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EnvelopeError { + Record(RecordError), + Crypto(CryptoError), + Keyring(KeyringError), + Content(ContentError), + WrongKind, + WrongRecipient, + WrongScope, + UnsupportedFormat, + SizeLimitExceeded, +} + +impl fmt::Display for EnvelopeError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for EnvelopeError {} +impl From for EnvelopeError { + fn from(error: RecordError) -> Self { + Self::Record(error) + } +} +impl From for EnvelopeError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} +impl From for EnvelopeError { + fn from(error: KeyringError) -> Self { + Self::Keyring(error) + } +} +impl From for EnvelopeError { + fn from(error: ContentError) -> Self { + Self::Content(error) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum RecipientKind { + Device = 1, + Recovery = 2, + Epoch = 3, +} + +impl TryFrom for RecipientKind { + type Error = EnvelopeError; + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(Self::Device), + 2 => Ok(Self::Recovery), + 3 => Ok(Self::Epoch), + _ => Err(EnvelopeError::UnsupportedFormat), + } + } +} + +pub struct SealedEnvelope { + binding: RecordBinding, + revision_id: [u8; 16], + encoded: Vec, +} + +impl SealedEnvelope { + pub fn binding(&self) -> &RecordBinding { + &self.binding + } + pub fn revision_id(&self) -> &[u8; 16] { + &self.revision_id + } + pub fn encoded(&self) -> &[u8] { + &self.encoded + } + pub fn into_encoded(self) -> Vec { + self.encoded + } +} + +impl fmt::Debug for SealedEnvelope { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("SealedEnvelope([REDACTED])") + } +} + +/// Epoch numbers occupy the recipient-id slot right-aligned. +pub fn epoch_recipient_id(epoch: u64) -> [u8; 16] { + let mut id = [0; 16]; + id[8..].copy_from_slice(&epoch.to_be_bytes()); + id +} + +/// Seal the whole keyring to `recipient` and sign the envelope. +pub fn seal_keyring( + binding: &RecordBinding, + recipient_kind: RecipientKind, + recipient_id: &[u8; 16], + recipient_key: &HpkePublicKey, + keyring: &Keyring, + signer: &DeviceSigner, +) -> Result { + check_binding(binding, signer)?; + if recipient_kind == RecipientKind::Epoch { + return Err(EnvelopeError::WrongRecipient); + } + let plaintext = Zeroizing::new(keyring.encode()); + if plaintext.len() > MAX_KEYRING_BYTES { + return Err(EnvelopeError::SizeLimitExceeded); + } + let revision_id = fresh_revision()?; + let context = record::context_bytes(binding, &revision_id)?; + let header = header(3, recipient_kind, recipient_id); + let info = concat(KEYRING_INFO_DOMAIN, &context, &header); + let sealed = hpke::seal(recipient_key, &info, &[], &plaintext, MAX_KEYRING_BYTES)?; + let mut payload = header_with_encapsulation(recipient_kind, recipient_id, &sealed.enc); + record::bytes_field(&mut payload, 4, &sealed.ciphertext); + finish(binding, revision_id, payload, signer) +} + +/// Verify and open a keyring envelope addressed to this recipient. +pub fn open_keyring( + encoded: &[u8], + expected: &RecordBinding, + recipient_kind: RecipientKind, + recipient_id: &[u8; 16], + recipient_key: &HpkePrivateKey, + trusted_public_key: &[u8], +) -> Result { + if expected.kind != RecordKind::KeyEnvelope { + return Err(EnvelopeError::WrongKind); + } + let record = UnverifiedRecord::parse(encoded, MAX_ENVELOPE_PAYLOAD_BYTES)? + .verify(expected, trusted_public_key)?; + let parsed = parse_payload(record.payload(), MAX_KEYRING_BYTES + 16)?; + if parsed.recipient_kind != recipient_kind || parsed.recipient_id != *recipient_id { + return Err(EnvelopeError::WrongRecipient); + } + if parsed.recipient_kind == RecipientKind::Epoch { + return Err(EnvelopeError::WrongRecipient); + } + let context = record::context_bytes(expected, record.revision_id())?; + let header = header(3, parsed.recipient_kind, &parsed.recipient_id); + let info = concat(KEYRING_INFO_DOMAIN, &context, &header); + let plaintext = hpke::open( + recipient_key, + &parsed.encapsulation, + &info, + &[], + parsed.ciphertext, + MAX_KEYRING_BYTES, + )?; + Ok(Keyring::decode(plaintext.as_bytes())?) +} + +/// Wrap one object's content key under the epoch key of `binding.epoch`. +/// The binding's object ID must be the key's object and the key's scope must +/// match the binding exactly. +pub fn wrap_object_key( + binding: &RecordBinding, + epoch_key: &[u8; 32], + key: &ContentKey, + signer: &DeviceSigner, +) -> Result { + check_binding(binding, signer)?; + if key.scope() != KeyScope::from(binding) { + return Err(EnvelopeError::WrongScope); + } + let recipient_id = epoch_recipient_id(binding.epoch); + let revision_id = fresh_revision()?; + let mut salt = [0; 32]; + crate::fill_random(&mut salt)?; + let context = record::context_bytes(binding, &revision_id)?; + let header = header_with_encapsulation(RecipientKind::Epoch, &recipient_id, &salt); + let derived = hkdf_sha256( + epoch_key, + &salt, + &concat(OBJECT_KEY_DOMAIN, &context, &header_prefix(&header)), + 32, + )?; + let aad = concat(OBJECT_AAD_DOMAIN, &context, &header_prefix(&header)); + let sealing_key = aead::LessSafeKey::new( + aead::UnboundKey::new(&aead::AES_256_GCM, derived.as_bytes()) + .map_err(|_| CryptoError::InvalidKeyLength)?, + ); + let mut buffer = Zeroizing::new(Vec::with_capacity(64)); + buffer.extend_from_slice(key.identifier()); + buffer.extend_from_slice(key.expose_secret()); + sealing_key + .seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key([0; 12]), + aead::Aad::from(&aad), + &mut *buffer, + ) + .map_err(|_| CryptoError::AuthenticationFailed)?; + let mut payload = header; + record::bytes_field(&mut payload, 4, &buffer); + finish(binding, revision_id, payload, signer) +} + +/// Verify and unwrap an object key envelope with the epoch key named by the +/// expected binding. +pub fn unwrap_object_key( + encoded: &[u8], + expected: &RecordBinding, + epoch_key: &[u8; 32], + trusted_public_key: &[u8], +) -> Result { + if expected.kind != RecordKind::KeyEnvelope { + return Err(EnvelopeError::WrongKind); + } + let record = UnverifiedRecord::parse(encoded, MAX_ENVELOPE_PAYLOAD_BYTES)? + .verify(expected, trusted_public_key)?; + let parsed = parse_payload(record.payload(), 48)?; + if parsed.recipient_kind != RecipientKind::Epoch + || parsed.recipient_id != epoch_recipient_id(expected.epoch) + { + return Err(EnvelopeError::WrongRecipient); + } + let context = record::context_bytes(expected, record.revision_id())?; + let header = header_with_encapsulation( + RecipientKind::Epoch, + &parsed.recipient_id, + &parsed.encapsulation, + ); + let derived = hkdf_sha256( + epoch_key, + &parsed.encapsulation, + &concat(OBJECT_KEY_DOMAIN, &context, &header_prefix(&header)), + 32, + )?; + let aad = concat(OBJECT_AAD_DOMAIN, &context, &header_prefix(&header)); + let plaintext = open_aes256_gcm(derived.as_bytes(), &[0; 12], &aad, parsed.ciphertext, 48)?; + let bytes = plaintext.as_bytes(); + if bytes.len() != 48 { + return Err(EnvelopeError::UnsupportedFormat); + } + let identifier: [u8; 16] = bytes[..16] + .try_into() + .map_err(|_| EnvelopeError::UnsupportedFormat)?; + Ok(ContentKey::from_bytes( + KeyScope::from(expected), + identifier, + &bytes[16..], + )?) +} + +struct ParsedPayload<'a> { + recipient_kind: RecipientKind, + recipient_id: [u8; 16], + encapsulation: [u8; 32], + ciphertext: &'a [u8], +} + +fn parse_payload(payload: &[u8], max_plaintext: usize) -> Result, EnvelopeError> { + let mut reader = Reader::new(payload); + if reader.argument(5)? != 5 { + return Err(EnvelopeError::UnsupportedFormat); + } + if reader.uint_field(0)? != 1 { + return Err(EnvelopeError::UnsupportedFormat); + } + let recipient_kind = RecipientKind::try_from(reader.uint_field(1)?)?; + let recipient_id = reader.fixed_field(2)?; + let encapsulation = reader.fixed_field(3)?; + let ciphertext = reader.bytes_field(4, max_plaintext + 16)?; + reader.finish()?; + if ciphertext.len() < 16 { + return Err(EnvelopeError::UnsupportedFormat); + } + Ok(ParsedPayload { + recipient_kind, + recipient_id, + encapsulation, + ciphertext, + }) +} + +fn check_binding(binding: &RecordBinding, signer: &DeviceSigner) -> Result<(), EnvelopeError> { + if binding.kind != RecordKind::KeyEnvelope { + return Err(EnvelopeError::WrongKind); + } + if binding.author_id != *signer.author_id() { + return Err(EnvelopeError::Content(ContentError::WrongAuthor)); + } + Ok(()) +} + +fn fresh_revision() -> Result<[u8; 16], EnvelopeError> { + let mut revision_id = [0; 16]; + crate::fill_random(&mut revision_id)?; + Ok(revision_id) +} + +fn header(count: u64, recipient_kind: RecipientKind, recipient_id: &[u8; 16]) -> Vec { + let mut out = Vec::with_capacity(96); + record::argument(&mut out, 5, count); + record::uint_field(&mut out, 0, 1); + record::uint_field(&mut out, 1, recipient_kind as u64); + record::bytes_field(&mut out, 2, recipient_id); + out +} + +fn header_with_encapsulation( + recipient_kind: RecipientKind, + recipient_id: &[u8; 16], + encapsulation: &[u8; 32], +) -> Vec { + let mut out = header(5, recipient_kind, recipient_id); + record::bytes_field(&mut out, 3, encapsulation); + out +} + +/// The object-key derivation binds fields 0..3 as a length-4 map, distinct +/// from the length-5 payload map that also carries the ciphertext. +fn header_prefix(header_with_encapsulation: &[u8]) -> Vec { + let mut out = header_with_encapsulation.to_vec(); + out[0] = 0xa4; + out +} + +fn concat(domain: &[u8], context: &[u8], header: &[u8]) -> Vec { + let mut out = Vec::with_capacity(domain.len() + context.len() + header.len()); + out.extend_from_slice(domain); + out.extend_from_slice(context); + out.extend_from_slice(header); + out +} + +fn finish( + binding: &RecordBinding, + revision_id: [u8; 16], + payload: Vec, + signer: &DeviceSigner, +) -> Result { + let input = record::signing_bytes(binding, &revision_id, &payload, MAX_ENVELOPE_PAYLOAD_BYTES)?; + let signature = signer.sign_bytes(&input)?; + let encoded = record::encode_signed( + binding, + &revision_id, + &payload, + &signature, + MAX_ENVELOPE_PAYLOAD_BYTES, + )?; + Ok(SealedEnvelope { + binding: *binding, + revision_id, + encoded, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn binding(kind: RecordKind, object_id: [u8; 16], epoch: u64) -> RecordBinding { + RecordBinding { + kind, + vault_id: [1; 16], + generation: [2; 16], + epoch, + object_id, + author_id: [4; 16], + membership_hash: [5; 32], + } + } + + #[test] + fn keyring_envelope_round_trips_and_binds_recipient() { + let signer = DeviceSigner::from_seed([4; 16], &[8; 32]).unwrap(); + let recipient = HpkePrivateKey::generate().unwrap(); + let other = HpkePrivateKey::generate().unwrap(); + let mut keyring = Keyring::new(); + keyring.insert_fresh(1).unwrap(); + keyring.insert_fresh(2).unwrap(); + let context = binding(RecordKind::KeyEnvelope, [0; 16], 2); + let sealed = seal_keyring( + &context, + RecipientKind::Device, + &[7; 16], + &recipient.public_key(), + &keyring, + &signer, + ) + .unwrap(); + let opened = open_keyring( + sealed.encoded(), + &context, + RecipientKind::Device, + &[7; 16], + &recipient, + signer.public_key(), + ) + .unwrap(); + assert_eq!(opened.epoch_key(1), keyring.epoch_key(1)); + assert_eq!(opened.epoch_key(2), keyring.epoch_key(2)); + assert!(matches!( + open_keyring( + sealed.encoded(), + &context, + RecipientKind::Recovery, + &[7; 16], + &recipient, + signer.public_key() + ), + Err(EnvelopeError::WrongRecipient) + )); + assert!(matches!( + open_keyring( + sealed.encoded(), + &context, + RecipientKind::Device, + &[8; 16], + &recipient, + signer.public_key() + ), + Err(EnvelopeError::WrongRecipient) + )); + assert!( + open_keyring( + sealed.encoded(), + &context, + RecipientKind::Device, + &[7; 16], + &other, + signer.public_key() + ) + .is_err() + ); + let mut wrong = context; + wrong.epoch = 3; + assert!(matches!( + open_keyring( + sealed.encoded(), + &wrong, + RecipientKind::Device, + &[7; 16], + &recipient, + signer.public_key() + ), + Err(EnvelopeError::Record(RecordError::ContextMismatch)) + )); + let mut tampered = sealed.encoded().to_vec(); + let last = tampered.len() - 70; + tampered[last] ^= 1; + assert!( + open_keyring( + &tampered, + &context, + RecipientKind::Device, + &[7; 16], + &recipient, + signer.public_key() + ) + .is_err() + ); + assert!(matches!( + seal_keyring( + &binding(RecordKind::Content, [0; 16], 2), + RecipientKind::Device, + &[7; 16], + &recipient.public_key(), + &keyring, + &signer + ), + Err(EnvelopeError::WrongKind) + )); + assert_eq!(format!("{sealed:?}"), "SealedEnvelope([REDACTED])"); + } + + #[test] + fn object_key_wrap_round_trips_and_binds_epoch_and_object() { + let signer = DeviceSigner::from_seed([4; 16], &[8; 32]).unwrap(); + let epoch_key = [11; 32]; + let context = binding(RecordKind::KeyEnvelope, [9; 16], 2); + let key = ContentKey::generate(KeyScope::from(&context)).unwrap(); + let wrapped = wrap_object_key(&context, &epoch_key, &key, &signer).unwrap(); + let unwrapped = + unwrap_object_key(wrapped.encoded(), &context, &epoch_key, signer.public_key()) + .unwrap(); + assert_eq!(unwrapped.identifier(), key.identifier()); + assert_eq!(unwrapped.expose_secret(), key.expose_secret()); + assert_eq!(unwrapped.scope(), key.scope()); + assert!( + unwrap_object_key(wrapped.encoded(), &context, &[12; 32], signer.public_key()).is_err() + ); + let mut other_object = context; + other_object.object_id = [10; 16]; + assert!( + unwrap_object_key( + wrapped.encoded(), + &other_object, + &epoch_key, + signer.public_key() + ) + .is_err() + ); + assert!(matches!( + wrap_object_key(&other_object, &epoch_key, &key, &signer), + Err(EnvelopeError::WrongScope) + )); + // A keyring envelope is not an object key and vice versa. + let recipient = HpkePrivateKey::generate().unwrap(); + assert!(matches!( + open_keyring( + wrapped.encoded(), + &context, + RecipientKind::Epoch, + &epoch_recipient_id(2), + &recipient, + signer.public_key() + ), + Err(EnvelopeError::WrongRecipient) + )); + let again = wrap_object_key(&context, &epoch_key, &key, &signer).unwrap(); + assert_ne!(again.encoded(), wrapped.encoded()); + } +} diff --git a/crates/crypto/src/hpke.rs b/crates/crypto/src/hpke.rs new file mode 100644 index 000000000..9c061ce0d --- /dev/null +++ b/crates/crypto/src/hpke.rs @@ -0,0 +1,468 @@ +//! RFC 9180 HPKE, base mode only, one fixed suite: DHKEM(X25519, HKDF-SHA256) +//! with HKDF-SHA256 and AES-256-GCM. This is the recipient-envelope primitive +//! from RFC 0001 §7.1: it encrypts to a public key and nothing more. +//! Encryption to a key does not identify the sender, so every envelope that +//! crosses the network is additionally wrapped in the signed record +//! (`record.rs`). +//! +//! The construction is assembled from reviewed primitives (ring's HMAC/HKDF/ +//! AES-GCM and curve25519-dalek's X25519 ladder); no curve or cipher code is +//! implemented here. Conformance is pinned against the RFC's A.1 vectors (the +//! KEM and key schedule are suite-independent; the AES-128 vector exercises +//! the identical code path with a narrower key). + +use crate::{CryptoError, SecretBytes}; +use curve25519_dalek::montgomery::MontgomeryPoint; +use ring::rand::SecureRandom; +use ring::{aead, hkdf, hmac}; +use zeroize::Zeroizing; + +const KEM_ID: u16 = 0x0020; +const KDF_ID: u16 = 0x0001; +const AEAD_AES_256_GCM: u16 = 0x0002; +#[cfg(test)] +const AEAD_AES_128_GCM: u16 = 0x0001; +const VERSION_LABEL: &[u8] = b"HPKE-v1"; +const MODE_BASE: u8 = 0; +const NONCE_LEN: usize = 12; +/// Envelopes carry keyrings and object keys, never bulk content. +pub const MAX_PLAINTEXT_BYTES: usize = 64 * 1024; + +/// X25519 private key. Debug output is redacted; bytes zeroize on drop. +pub struct HpkePrivateKey(Zeroizing<[u8; 32]>); + +/// X25519 public key (32-byte u-coordinate). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct HpkePublicKey([u8; 32]); + +impl HpkePrivateKey { + pub fn generate() -> Result { + let mut bytes = Zeroizing::new([0; 32]); + ring::rand::SystemRandom::new() + .fill(bytes.as_mut()) + .map_err(|_| CryptoError::EntropyUnavailable)?; + Ok(Self(bytes)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes: [u8; 32] = bytes + .try_into() + .map_err(|_| CryptoError::InvalidKeyLength)?; + Ok(Self(Zeroizing::new(bytes))) + } + + pub fn public_key(&self) -> HpkePublicKey { + HpkePublicKey(MontgomeryPoint::mul_base_clamped(*self.0).to_bytes()) + } + + pub fn expose_secret(&self) -> &[u8; 32] { + &self.0 + } + + fn diffie_hellman(&self, peer: &HpkePublicKey) -> Result, CryptoError> { + let shared = Zeroizing::new(MontgomeryPoint(peer.0).mul_clamped(*self.0).to_bytes()); + // RFC 7748 §6.1 / RFC 9180 §4.1: a low-order peer point yields the + // all-zero output and MUST be rejected. + if shared.iter().all(|byte| *byte == 0) { + return Err(CryptoError::InvalidPublicKey); + } + Ok(shared) + } +} + +impl std::fmt::Debug for HpkePrivateKey { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("HpkePrivateKey([REDACTED])") + } +} + +impl HpkePublicKey { + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes: [u8; 32] = bytes + .try_into() + .map_err(|_| CryptoError::InvalidKeyLength)?; + Ok(Self(bytes)) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// A single-shot base-mode ciphertext plus its encapsulated key. +#[derive(Clone, PartialEq, Eq)] +pub struct HpkeSealed { + pub enc: [u8; 32], + pub ciphertext: Vec, +} + +impl std::fmt::Debug for HpkeSealed { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("HpkeSealed([REDACTED])") + } +} + +/// Encrypt `plaintext` to `recipient` (RFC 9180 §6.1 single-shot, seq 0). +pub fn seal( + recipient: &HpkePublicKey, + info: &[u8], + aad: &[u8], + plaintext: &[u8], + max_plaintext_bytes: usize, +) -> Result { + let ephemeral = HpkePrivateKey::generate()?; + seal_with_ephemeral( + &ephemeral, + recipient, + info, + aad, + plaintext, + max_plaintext_bytes, + ) +} + +fn seal_with_ephemeral( + ephemeral: &HpkePrivateKey, + recipient: &HpkePublicKey, + info: &[u8], + aad: &[u8], + plaintext: &[u8], + max_plaintext_bytes: usize, +) -> Result { + if max_plaintext_bytes > MAX_PLAINTEXT_BYTES || plaintext.len() > max_plaintext_bytes { + return Err(CryptoError::SizeLimitExceeded); + } + let enc = ephemeral.public_key(); + let shared_secret = encapsulated_secret(ephemeral.diffie_hellman(recipient)?, &enc, recipient)?; + let schedule = key_schedule(&aead::AES_256_GCM, AEAD_AES_256_GCM, &shared_secret, info)?; + let key = aead::LessSafeKey::new( + aead::UnboundKey::new(&aead::AES_256_GCM, schedule.key.as_bytes()) + .map_err(|_| CryptoError::InvalidKeyLength)?, + ); + let mut buffer = Vec::with_capacity(plaintext.len() + aead::AES_256_GCM.tag_len()); + buffer.extend_from_slice(plaintext); + key.seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(schedule.base_nonce), + aead::Aad::from(aad), + &mut buffer, + ) + .map_err(|_| CryptoError::AuthenticationFailed)?; + Ok(HpkeSealed { + enc: enc.0, + ciphertext: buffer, + }) +} + +/// Decrypt a single-shot base-mode ciphertext addressed to `recipient`. +pub fn open( + recipient: &HpkePrivateKey, + enc: &[u8; 32], + info: &[u8], + aad: &[u8], + ciphertext: &[u8], + max_plaintext_bytes: usize, +) -> Result { + if max_plaintext_bytes > MAX_PLAINTEXT_BYTES { + return Err(CryptoError::SizeLimitExceeded); + } + let ephemeral_public = HpkePublicKey(*enc); + let shared_secret = encapsulated_secret( + recipient.diffie_hellman(&ephemeral_public)?, + &ephemeral_public, + &recipient.public_key(), + )?; + let schedule = key_schedule(&aead::AES_256_GCM, AEAD_AES_256_GCM, &shared_secret, info)?; + crate::open_aes256_gcm( + schedule.key.as_bytes(), + &schedule.base_nonce, + aad, + ciphertext, + max_plaintext_bytes, + ) +} + +struct Schedule { + key: SecretBytes, + base_nonce: [u8; NONCE_LEN], +} + +fn kem_suite_id() -> [u8; 5] { + let mut id = *b"KEM\0\0"; + id[3..].copy_from_slice(&KEM_ID.to_be_bytes()); + id +} + +fn hpke_suite_id(aead_id: u16) -> [u8; 10] { + let mut id = *b"HPKE\0\0\0\0\0\0"; + id[4..6].copy_from_slice(&KEM_ID.to_be_bytes()); + id[6..8].copy_from_slice(&KDF_ID.to_be_bytes()); + id[8..].copy_from_slice(&aead_id.to_be_bytes()); + id +} + +/// RFC 9180 §4: `LabeledExtract(salt, label, ikm)`; the PRK is returned as raw +/// HMAC output because the key-schedule context embeds two of them. +fn labeled_extract(suite_id: &[u8], salt: &[u8], label: &[u8], ikm: &[u8]) -> Zeroizing<[u8; 32]> { + let key = hmac::Key::new(hmac::HMAC_SHA256, salt); + let mut context = hmac::Context::with_key(&key); + context.update(VERSION_LABEL); + context.update(suite_id); + context.update(label); + context.update(ikm); + let tag = context.sign(); + let mut prk = Zeroizing::new([0; 32]); + prk.copy_from_slice(tag.as_ref()); + prk +} + +/// RFC 9180 §4: `LabeledExpand(prk, label, info, L)`. +fn labeled_expand( + suite_id: &[u8], + prk: &[u8; 32], + label: &[u8], + info: &[u8], + length: usize, +) -> Result { + struct Length(usize); + impl hkdf::KeyType for Length { + fn len(&self) -> usize { + self.0 + } + } + let length_prefix = u16::try_from(length) + .map_err(|_| CryptoError::InvalidOutputLength)? + .to_be_bytes(); + let prk = hkdf::Prk::new_less_safe(hkdf::HKDF_SHA256, prk); + let parts: [&[u8]; 5] = [&length_prefix, VERSION_LABEL, suite_id, label, info]; + let okm = prk + .expand(&parts, Length(length)) + .map_err(|_| CryptoError::InvalidOutputLength)?; + let mut output = Zeroizing::new(vec![0; length]); + okm.fill(&mut output) + .map_err(|_| CryptoError::InvalidOutputLength)?; + Ok(SecretBytes::new(output)) +} + +/// RFC 9180 §4.1 `ExtractAndExpand(dh, kem_context)` for DHKEM(X25519). +fn encapsulated_secret( + dh: Zeroizing<[u8; 32]>, + enc: &HpkePublicKey, + recipient: &HpkePublicKey, +) -> Result { + let suite = kem_suite_id(); + let eae_prk = labeled_extract(&suite, &[], b"eae_prk", dh.as_ref()); + let mut kem_context = [0; 64]; + kem_context[..32].copy_from_slice(&enc.0); + kem_context[32..].copy_from_slice(&recipient.0); + labeled_expand(&suite, &eae_prk, b"shared_secret", &kem_context, 32) +} + +/// RFC 9180 §5.1 `KeySchedule` for base mode (no PSK). +fn key_schedule( + algorithm: &'static aead::Algorithm, + aead_id: u16, + shared_secret: &SecretBytes, + info: &[u8], +) -> Result { + let suite = hpke_suite_id(aead_id); + let psk_id_hash = labeled_extract(&suite, &[], b"psk_id_hash", &[]); + let info_hash = labeled_extract(&suite, &[], b"info_hash", info); + let mut context = Vec::with_capacity(65); + context.push(MODE_BASE); + context.extend_from_slice(psk_id_hash.as_ref()); + context.extend_from_slice(info_hash.as_ref()); + let secret = labeled_extract(&suite, shared_secret.as_bytes(), b"secret", &[]); + let key = labeled_expand(&suite, &secret, b"key", &context, algorithm.key_len())?; + let nonce = labeled_expand(&suite, &secret, b"base_nonce", &context, NONCE_LEN)?; + let mut base_nonce = [0; NONCE_LEN]; + base_nonce.copy_from_slice(nonce.as_bytes()); + Ok(Schedule { key, base_nonce }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hex(text: &str) -> Vec { + (0..text.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&text[index..index + 2], 16).unwrap()) + .collect() + } + + /// RFC 9180 Appendix A.1.1: DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, + /// AES-128-GCM, base mode. The KEM and key schedule are the exact code + /// used by the production AES-256 suite; only the AEAD width differs. + #[test] + fn matches_rfc9180_a1_base_vectors() { + let sk_e = HpkePrivateKey::from_bytes(&hex( + "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736", + )) + .unwrap(); + let sk_r = HpkePrivateKey::from_bytes(&hex( + "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8", + )) + .unwrap(); + let pk_r = sk_r.public_key(); + assert_eq!( + pk_r.as_bytes().to_vec(), + hex("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d") + ); + let enc = sk_e.public_key(); + assert_eq!( + enc.as_bytes().to_vec(), + hex("37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431") + ); + let shared = encapsulated_secret(sk_e.diffie_hellman(&pk_r).unwrap(), &enc, &pk_r).unwrap(); + assert_eq!( + shared.as_bytes().to_vec(), + hex("fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc") + ); + // Decapsulation on the recipient side reaches the same secret. + let decapsulated = + encapsulated_secret(sk_r.diffie_hellman(&enc).unwrap(), &enc, &pk_r).unwrap(); + assert_eq!(decapsulated.as_bytes(), shared.as_bytes()); + + let info = hex("4f6465206f6e2061204772656369616e2055726e"); + let schedule = key_schedule(&aead::AES_128_GCM, AEAD_AES_128_GCM, &shared, &info).unwrap(); + assert_eq!( + schedule.key.as_bytes().to_vec(), + hex("4531685d41d65f03dc48f6b8302c05b0") + ); + assert_eq!( + schedule.base_nonce.to_vec(), + hex("56d890e5accaaf011cff4b7d") + ); + + // Sequence-0 encryption with the derived key and nonce. + let key = aead::LessSafeKey::new( + aead::UnboundKey::new(&aead::AES_128_GCM, schedule.key.as_bytes()).unwrap(), + ); + let mut buffer = hex("4265617574792069732074727574682c20747275746820626561757479"); + key.seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(schedule.base_nonce), + aead::Aad::from(hex("436f756e742d30")), + &mut buffer, + ) + .unwrap(); + assert_eq!( + buffer, + hex( + "f938558b5d72f1a23810b4be2ab4f84331acc02fc97babc53a52ae8218a355a96d8770ac83d07bea87e13c512a" + ) + ); + } + + #[test] + fn round_trips_and_binds_info_aad_and_recipient() { + let recipient = HpkePrivateKey::generate().unwrap(); + let other = HpkePrivateKey::generate().unwrap(); + let sealed = seal( + &recipient.public_key(), + b"info", + b"aad", + b"keyring bytes", + 1024, + ) + .unwrap(); + let opened = open( + &recipient, + &sealed.enc, + b"info", + b"aad", + &sealed.ciphertext, + 1024, + ) + .unwrap(); + assert_eq!(opened.as_bytes(), b"keyring bytes"); + assert!( + open( + &other, + &sealed.enc, + b"info", + b"aad", + &sealed.ciphertext, + 1024 + ) + .is_err() + ); + assert!( + open( + &recipient, + &sealed.enc, + b"INFO", + b"aad", + &sealed.ciphertext, + 1024 + ) + .is_err() + ); + assert!( + open( + &recipient, + &sealed.enc, + b"info", + b"AAD", + &sealed.ciphertext, + 1024 + ) + .is_err() + ); + let mut tampered = sealed.ciphertext.clone(); + tampered[0] ^= 1; + assert!(open(&recipient, &sealed.enc, b"info", b"aad", &tampered, 1024).is_err()); + assert!(matches!( + open( + &recipient, + &sealed.enc, + b"info", + b"aad", + &sealed.ciphertext, + 4 + ), + Err(CryptoError::SizeLimitExceeded) + )); + // Two seals of one plaintext never share an encapsulation or bytes. + let again = seal( + &recipient.public_key(), + b"info", + b"aad", + b"keyring bytes", + 1024, + ) + .unwrap(); + assert_ne!(again.enc, sealed.enc); + assert_ne!(again.ciphertext, sealed.ciphertext); + } + + #[test] + fn rejects_low_order_peer_points_and_oversized_input() { + let recipient = HpkePrivateKey::generate().unwrap(); + let zero_point = HpkePublicKey([0; 32]); + assert!(matches!( + seal(&zero_point, b"", b"", b"x", 16), + Err(CryptoError::InvalidPublicKey) + )); + let sealed = seal(&recipient.public_key(), b"", b"", b"x", 16).unwrap(); + assert!(matches!( + open(&recipient, &[0; 32], b"", b"", &sealed.ciphertext, 16), + Err(CryptoError::InvalidPublicKey) + )); + assert!(matches!( + seal(&recipient.public_key(), b"", b"", &[0; 17], 16), + Err(CryptoError::SizeLimitExceeded) + )); + assert!(matches!( + seal( + &recipient.public_key(), + b"", + b"", + b"", + MAX_PLAINTEXT_BYTES + 1 + ), + Err(CryptoError::SizeLimitExceeded) + )); + assert!(HpkePrivateKey::from_bytes(&[1; 31]).is_err()); + assert_eq!(format!("{recipient:?}"), "HpkePrivateKey([REDACTED])"); + } +} diff --git a/crates/crypto/src/keyring.rs b/crates/crypto/src/keyring.rs new file mode 100644 index 000000000..9edb8741e --- /dev/null +++ b/crates/crypto/src/keyring.rs @@ -0,0 +1,238 @@ +//! The workspace keyring (RFC 0001 §5): one random 32-byte wrapping key per +//! write epoch. Devices and the recovery authority receive the whole keyring +//! inside an HPKE envelope (`envelope.rs`); per-object content keys are +//! wrapped under the epoch key of the epoch that created them. Keys are +//! never discarded while ciphertext may still reference them. +//! +//! Encoding: deterministic CBOR map `{0: 1, 1: [[epoch, key32], ...]}` with +//! epochs strictly ascending. + +use crate::CryptoError; +use crate::record::{self, Reader, RecordError}; +use std::collections::BTreeMap; +use std::fmt; +use zeroize::Zeroizing; + +pub const MAX_EPOCHS: usize = 1024; +pub const MAX_KEYRING_BYTES: usize = 16 + MAX_EPOCHS * 44; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum KeyringError { + Record(RecordError), + Crypto(CryptoError), + Malformed, + UnsupportedVersion, + DuplicateEpoch, + InvalidEpoch, + TooManyEpochs, +} + +impl fmt::Display for KeyringError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for KeyringError {} +impl From for KeyringError { + fn from(error: RecordError) -> Self { + Self::Record(error) + } +} +impl From for KeyringError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} + +#[derive(Default)] +pub struct Keyring { + epochs: BTreeMap>, +} + +impl Keyring { + pub fn new() -> Self { + Self::default() + } + + /// Generate a fresh random key for `epoch`; an existing epoch is never + /// overwritten (a rotation always introduces a NEW epoch number). + pub fn insert_fresh(&mut self, epoch: u64) -> Result<(), KeyringError> { + if epoch == 0 { + return Err(KeyringError::InvalidEpoch); + } + if self.epochs.contains_key(&epoch) { + return Err(KeyringError::DuplicateEpoch); + } + if self.epochs.len() >= MAX_EPOCHS { + return Err(KeyringError::TooManyEpochs); + } + let mut key = Zeroizing::new([0; 32]); + crate::fill_random(key.as_mut())?; + self.epochs.insert(epoch, key); + Ok(()) + } + + pub fn insert(&mut self, epoch: u64, key: &[u8]) -> Result<(), KeyringError> { + if epoch == 0 { + return Err(KeyringError::InvalidEpoch); + } + let key: [u8; 32] = key.try_into().map_err(|_| KeyringError::Malformed)?; + match self.epochs.get(&epoch) { + Some(existing) if **existing == key => Ok(()), + Some(_) => Err(KeyringError::DuplicateEpoch), + None if self.epochs.len() >= MAX_EPOCHS => Err(KeyringError::TooManyEpochs), + None => { + self.epochs.insert(epoch, Zeroizing::new(key)); + Ok(()) + } + } + } + + /// Merge every epoch from `other`; conflicting bytes for one epoch fail. + pub fn merge(&mut self, other: &Keyring) -> Result<(), KeyringError> { + for (epoch, key) in &other.epochs { + self.insert(*epoch, key.as_ref())?; + } + Ok(()) + } + + pub fn epoch_key(&self, epoch: u64) -> Option<&[u8; 32]> { + self.epochs.get(&epoch).map(|key| &**key) + } + + pub fn epochs(&self) -> impl Iterator + '_ { + self.epochs.keys().copied() + } + + pub fn latest_epoch(&self) -> Option { + self.epochs.keys().next_back().copied() + } + + pub fn len(&self) -> usize { + self.epochs.len() + } + + pub fn is_empty(&self) -> bool { + self.epochs.is_empty() + } + + pub fn encode(&self) -> Vec { + let mut out = Vec::with_capacity(16 + self.epochs.len() * 44); + record::argument(&mut out, 5, 2); + record::uint_field(&mut out, 0, 1); + record::argument(&mut out, 0, 1); + record::argument(&mut out, 4, self.epochs.len() as u64); + for (epoch, key) in &self.epochs { + record::argument(&mut out, 4, 2); + record::argument(&mut out, 0, *epoch); + record::argument(&mut out, 2, 32); + out.extend_from_slice(key.as_ref()); + } + out + } + + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_KEYRING_BYTES { + return Err(KeyringError::TooManyEpochs); + } + let mut reader = Reader::new(bytes); + if reader.argument(5)? != 2 { + return Err(KeyringError::Malformed); + } + if reader.uint_field(0)? != 1 { + return Err(KeyringError::UnsupportedVersion); + } + if reader.argument(0)? != 1 { + return Err(KeyringError::Malformed); + } + let count = reader.argument(4)?; + if count > MAX_EPOCHS as u64 { + return Err(KeyringError::TooManyEpochs); + } + let mut keyring = Self::new(); + let mut previous = 0; + for _ in 0..count { + if reader.argument(4)? != 2 { + return Err(KeyringError::Malformed); + } + let epoch = reader.argument(0)?; + if epoch <= previous { + return Err(KeyringError::Malformed); + } + previous = epoch; + let key = reader.fixed_bytes::<32>()?; + keyring.epochs.insert(epoch, Zeroizing::new(key)); + } + reader.finish()?; + Ok(keyring) + } +} + +impl fmt::Debug for Keyring { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "Keyring({} epochs, [REDACTED])", + self.epochs.len() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trips_and_rejects_malformed() { + let mut keyring = Keyring::new(); + keyring.insert_fresh(1).unwrap(); + keyring.insert_fresh(3).unwrap(); + assert!(matches!( + keyring.insert_fresh(1), + Err(KeyringError::DuplicateEpoch) + )); + assert!(matches!( + keyring.insert_fresh(0), + Err(KeyringError::InvalidEpoch) + )); + let encoded = keyring.encode(); + let decoded = Keyring::decode(&encoded).unwrap(); + assert_eq!(decoded.epoch_key(1), keyring.epoch_key(1)); + assert_eq!(decoded.epoch_key(3), keyring.epoch_key(3)); + assert_eq!(decoded.latest_epoch(), Some(3)); + assert_eq!(decoded.epochs().collect::>(), vec![1, 3]); + assert!(Keyring::decode(&encoded[..encoded.len() - 1]).is_err()); + let mut trailing = encoded.clone(); + trailing.push(0); + assert!(Keyring::decode(&trailing).is_err()); + // Descending epochs are non-canonical. + let mut swapped = Keyring::new(); + swapped.insert(3, keyring.epoch_key(3).unwrap()).unwrap(); + swapped.insert(1, keyring.epoch_key(1).unwrap()).unwrap(); + assert_eq!(swapped.encode(), encoded, "BTreeMap orders on encode"); + let mut bytes = encoded.clone(); + // Patch epoch 1 -> 4 (single byte, < 24) makes the order 4, 3. + let position = bytes.iter().position(|byte| *byte == 0x01).unwrap(); + let _ = position; + bytes[8] = 0x04; + assert!(Keyring::decode(&bytes).is_err()); + assert_eq!(format!("{keyring:?}"), "Keyring(2 epochs, [REDACTED])"); + } + + #[test] + fn merge_keeps_history_and_detects_conflicts() { + let mut first = Keyring::new(); + first.insert_fresh(1).unwrap(); + let mut second = Keyring::new(); + second.insert_fresh(2).unwrap(); + second.merge(&first).unwrap(); + assert_eq!(second.len(), 2); + let mut conflicting = Keyring::new(); + conflicting.insert_fresh(1).unwrap(); + assert!(matches!( + second.merge(&conflicting), + Err(KeyringError::DuplicateEpoch) + )); + } +} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs new file mode 100644 index 000000000..f15e5cdc6 --- /dev/null +++ b/crates/crypto/src/lib.rs @@ -0,0 +1,239 @@ +#![forbid(unsafe_code)] + +pub mod channel; +pub mod content; +pub mod envelope; +pub mod hpke; +pub mod keyring; +pub mod policy; +pub mod record; +pub mod recovery; + +use ring::{aead, hkdf, signature}; +use std::fmt; +use zeroize::Zeroizing; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CryptoError { + InvalidKeyLength, + InvalidNonceLength, + InvalidCiphertextLength, + InvalidSignatureLength, + InvalidOutputLength, + SizeLimitExceeded, + AuthenticationFailed, + InvalidPublicKey, + EntropyUnavailable, +} + +impl fmt::Display for CryptoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for CryptoError {} + +pub struct SecretBytes(Zeroizing>); + +impl SecretBytes { + pub(crate) fn new(bytes: Zeroizing>) -> Self { + Self(bytes) + } + + /// Copy `bytes` into a zeroizing allocation. + pub fn from_slice(bytes: &[u8]) -> Self { + Self(Zeroizing::new(bytes.to_vec())) + } + + /// Explicit duplication (deliberately not `Clone`: copies of secrets + /// should be visible in code review). + pub fn try_clone(&self) -> Result { + Ok(Self::from_slice(&self.0)) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +/// SHA-256 over `parts` in order — the only digest this crate exposes, used +/// for membership hashes and identifier derivation. +pub fn sha256(parts: &[&[u8]]) -> [u8; 32] { + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + for part in parts { + context.update(part); + } + let mut output = [0; 32]; + output.copy_from_slice(context.finish().as_ref()); + output +} + +/// Fill `buffer` from the OS entropy source. +pub fn fill_random(buffer: &mut [u8]) -> Result<(), CryptoError> { + ring::rand::SecureRandom::fill(&ring::rand::SystemRandom::new(), buffer) + .map_err(|_| CryptoError::EntropyUnavailable) +} + +impl fmt::Debug for SecretBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SecretBytes([REDACTED])") + } +} + +pub fn open_aes256_gcm( + key: &[u8], + nonce: &[u8], + aad: &[u8], + ciphertext_and_tag: &[u8], + max_plaintext_bytes: usize, +) -> Result { + if key.len() != 32 { + return Err(CryptoError::InvalidKeyLength); + } + let nonce = aead::Nonce::try_assume_unique_for_key(nonce) + .map_err(|_| CryptoError::InvalidNonceLength)?; + let plaintext_len = ciphertext_and_tag + .len() + .checked_sub(aead::AES_256_GCM.tag_len()) + .ok_or(CryptoError::InvalidCiphertextLength)?; + if plaintext_len > max_plaintext_bytes { + return Err(CryptoError::SizeLimitExceeded); + } + let key = aead::LessSafeKey::new( + aead::UnboundKey::new(&aead::AES_256_GCM, key) + .map_err(|_| CryptoError::InvalidKeyLength)?, + ); + let mut buffer = Zeroizing::new(ciphertext_and_tag.to_vec()); + key.open_in_place(nonce, aead::Aad::from(aad), &mut buffer) + .map_err(|_| CryptoError::AuthenticationFailed)?; + buffer.truncate(plaintext_len); + Ok(SecretBytes(buffer)) +} + +/// AES-256-GCM with a caller-supplied nonce. Callers own nonce uniqueness; +/// content records never use this directly (they derive one key per record +/// and use a fixed nonce). Used for the local encrypted state file. +pub fn seal_aes256_gcm( + key: &[u8], + nonce: &[u8], + aad: &[u8], + plaintext: &[u8], +) -> Result, CryptoError> { + if key.len() != 32 { + return Err(CryptoError::InvalidKeyLength); + } + let nonce = aead::Nonce::try_assume_unique_for_key(nonce) + .map_err(|_| CryptoError::InvalidNonceLength)?; + let key = aead::LessSafeKey::new( + aead::UnboundKey::new(&aead::AES_256_GCM, key) + .map_err(|_| CryptoError::InvalidKeyLength)?, + ); + let mut buffer = Vec::with_capacity(plaintext.len() + aead::AES_256_GCM.tag_len()); + buffer.extend_from_slice(plaintext); + key.seal_in_place_append_tag(nonce, aead::Aad::from(aad), &mut buffer) + .map_err(|_| CryptoError::AuthenticationFailed)?; + Ok(buffer) +} + +pub fn verify_ed25519( + public_key: &[u8], + message: &[u8], + signature: &[u8], +) -> Result<(), CryptoError> { + if public_key.len() != 32 { + return Err(CryptoError::InvalidKeyLength); + } + if signature.len() != 64 { + return Err(CryptoError::InvalidSignatureLength); + } + if !ed25519_point_encoding_precheck(public_key) + || !ed25519_point_encoding_precheck(&signature[..32]) + || !ed25519_scalar_encoding_precheck(&signature[32..]) + { + return Err(CryptoError::AuthenticationFailed); + } + signature::UnparsedPublicKey::new(&signature::ED25519, public_key) + .verify(message, signature) + .map_err(|_| CryptoError::AuthenticationFailed) +} + +const ED25519_FIELD_MODULUS: [u8; 32] = { + let mut bytes = [0xff; 32]; + bytes[0] = 0xed; + bytes[31] = 0x7f; + bytes +}; + +const ED25519_SMALL_ORDER_Y: [[u8; 32]; 5] = [ + [0; 32], + { + let mut bytes = [0; 32]; + bytes[0] = 1; + bytes + }, + { + let mut bytes = ED25519_FIELD_MODULUS; + bytes[0] = 0xec; + bytes + }, + [ + 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, + 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, + 0xfc, 0x05, + ], + [ + 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, + 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, + 0x03, 0x7a, + ], +]; + +const ED25519_SCALAR_ORDER: [u8; 32] = [ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, +]; + +pub(crate) fn ed25519_point_encoding_precheck(encoded: &[u8]) -> bool { + let Ok(mut y) = <[u8; 32]>::try_from(encoded) else { + return false; + }; + y[31] &= 0x7f; + y.iter().rev().lt(ED25519_FIELD_MODULUS.iter().rev()) && !ED25519_SMALL_ORDER_Y.contains(&y) +} + +pub(crate) fn ed25519_scalar_encoding_precheck(encoded: &[u8]) -> bool { + encoded.len() == 32 && encoded.iter().rev().lt(ED25519_SCALAR_ORDER.iter().rev()) +} + +pub fn hkdf_sha256( + input_key_material: &[u8], + salt: &[u8], + info: &[u8], + output_len: usize, +) -> Result { + if !(1..=255 * 32).contains(&output_len) { + return Err(CryptoError::InvalidOutputLength); + } + struct OutputLength(usize); + impl hkdf::KeyType for OutputLength { + fn len(&self) -> usize { + self.0 + } + } + let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, salt).extract(input_key_material); + let info = [info]; + let okm = prk + .expand(&info, OutputLength(output_len)) + .map_err(|_| CryptoError::InvalidOutputLength)?; + let mut output = Zeroizing::new(vec![0; output_len]); + okm.fill(&mut output) + .map_err(|_| CryptoError::InvalidOutputLength)?; + Ok(SecretBytes(output)) +} + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod record_tests; diff --git a/crates/crypto/src/policy.rs b/crates/crypto/src/policy.rs new file mode 100644 index 000000000..f2add6662 --- /dev/null +++ b/crates/crypto/src/policy.rs @@ -0,0 +1,747 @@ +//! Signed membership policy records (RFC 0001 §5, §11): the genesis record +//! and the parent-linked history of device additions, revocations, recovery +//! authority changes, and write-epoch transitions. A verified +//! [`MembershipState`] is the ONLY source of "who may sign for this vault"; +//! every content/envelope verification takes its expected binding and the +//! author's public key from here, never from the record being checked. +//! +//! Wire form: the signed wrapper (`record.rs`, kind = Policy) whose payload +//! is one deterministic CBOR map: +//! +//! ```text +//! 0 policy version unsigned, exactly 1 +//! 1 sequence unsigned; genesis = 0, then parent + 1 +//! 2 parent membership hash 32 bytes; genesis = zeros +//! 3 profile hash 32 bytes (SHA-256 of the org/user labels) +//! 4 active write epoch unsigned ≥ 1 +//! 5 operation 1 genesis, 2 add device, 3 revoke device, +//! 4 rotate recovery, 5 recovery transition +//! 6 recovery signing key 32 bytes (Ed25519) +//! 7 recovery encryption key 32 bytes (X25519) +//! 8 devices array of [id 16, signing 32, encryption 32, +//! status unsigned (0 active, 1 revoked)] +//! ``` +//! +//! The membership hash of a record is SHA-256("zeron/membership/v1" || 0x00 +//! || complete signed record bytes). Wrapper fields for policy records: +//! object ID = [`POLICY_OBJECT_ID`], epoch = the record's active epoch, +//! membership hash = the PARENT's hash (genesis: zeros). + +use crate::content::DeviceSigner; +use crate::record::{self, Reader, RecordBinding, RecordError, RecordKind, UnverifiedRecord}; +use crate::{CryptoError, sha256}; +use std::fmt; + +pub const POLICY_OBJECT_ID: [u8; 16] = [0; 16]; +pub const MAX_POLICY_BYTES: usize = 64 * 1024; +pub const MAX_DEVICES: usize = 64; +const MEMBERSHIP_DOMAIN: &[u8] = b"zeron/membership/v1\0"; +const PROFILE_DOMAIN: &[u8] = b"zeron/profile/v1\0"; +const RECOVERY_ID_DOMAIN: &[u8] = b"zeron/recovery-id/v1\0"; +const ENROLL_DOMAIN: &[u8] = b"zeron/enroll/v1\0"; +const PAIRING_DOMAIN: &[u8] = b"zeron/pairing-code/v1\0"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PolicyError { + Record(RecordError), + Crypto(CryptoError), + Malformed, + UnsupportedVersion, + UnsupportedOperation, + WrongVault, + WrongProfile, + WrongSequence, + WrongParent, + WrongEpoch, + UnknownAuthor, + RevokedAuthor, + InvalidDeviceSet, + InvalidRecoveryKeys, + TooManyDevices, + InvalidTransition, +} + +impl fmt::Display for PolicyError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for PolicyError {} +impl From for PolicyError { + fn from(error: RecordError) -> Self { + Self::Record(error) + } +} +impl From for PolicyError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} +impl From for PolicyError { + fn from(_: crate::content::ContentError) -> Self { + Self::Malformed + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum DeviceStatus { + Active = 0, + Revoked = 1, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum Operation { + Genesis = 1, + AddDevice = 2, + RevokeDevice = 3, + RotateRecovery = 4, + RecoveryTransition = 5, +} + +impl TryFrom for Operation { + type Error = PolicyError; + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(Self::Genesis), + 2 => Ok(Self::AddDevice), + 3 => Ok(Self::RevokeDevice), + 4 => Ok(Self::RotateRecovery), + 5 => Ok(Self::RecoveryTransition), + _ => Err(PolicyError::UnsupportedOperation), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DeviceEntry { + pub device_id: [u8; 16], + pub signing_key: [u8; 32], + pub encryption_key: [u8; 32], + pub status: DeviceStatus, +} + +/// The decoded policy payload. Public because the control plane and native +/// clients build these; only [`MembershipState`] decides whether one is valid. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyPayload { + pub sequence: u64, + pub parent_hash: [u8; 32], + pub profile_hash: [u8; 32], + pub epoch: u64, + pub operation: Operation, + pub recovery_signing_key: [u8; 32], + pub recovery_encryption_key: [u8; 32], + pub devices: Vec, +} + +impl PolicyPayload { + pub fn encode(&self) -> Result, PolicyError> { + if self.devices.len() > MAX_DEVICES { + return Err(PolicyError::TooManyDevices); + } + let mut out = Vec::with_capacity(256 + self.devices.len() * 96); + record::argument(&mut out, 5, 9); + record::uint_field(&mut out, 0, 1); + record::uint_field(&mut out, 1, self.sequence); + record::bytes_field(&mut out, 2, &self.parent_hash); + record::bytes_field(&mut out, 3, &self.profile_hash); + record::uint_field(&mut out, 4, self.epoch); + record::uint_field(&mut out, 5, self.operation as u64); + record::bytes_field(&mut out, 6, &self.recovery_signing_key); + record::bytes_field(&mut out, 7, &self.recovery_encryption_key); + record::argument(&mut out, 0, 8); + record::argument(&mut out, 4, self.devices.len() as u64); + for device in &self.devices { + record::argument(&mut out, 4, 4); + record::argument(&mut out, 2, 16); + out.extend_from_slice(&device.device_id); + record::argument(&mut out, 2, 32); + out.extend_from_slice(&device.signing_key); + record::argument(&mut out, 2, 32); + out.extend_from_slice(&device.encryption_key); + record::argument(&mut out, 0, device.status as u64); + } + Ok(out) + } + + pub fn decode(bytes: &[u8]) -> Result { + let mut reader = Reader::new(bytes); + if reader.argument(5)? != 9 { + return Err(PolicyError::Malformed); + } + if reader.uint_field(0)? != 1 { + return Err(PolicyError::UnsupportedVersion); + } + let sequence = reader.uint_field(1)?; + let parent_hash = reader.fixed_field(2)?; + let profile_hash = reader.fixed_field(3)?; + let epoch = reader.uint_field(4)?; + let operation = Operation::try_from(reader.uint_field(5)?)?; + let recovery_signing_key = reader.fixed_field(6)?; + let recovery_encryption_key = reader.fixed_field(7)?; + if reader.argument(0)? != 8 { + return Err(PolicyError::Malformed); + } + let count = reader.argument(4)?; + if count > MAX_DEVICES as u64 { + return Err(PolicyError::TooManyDevices); + } + let mut devices = Vec::with_capacity(count as usize); + for _ in 0..count { + if reader.argument(4)? != 4 { + return Err(PolicyError::Malformed); + } + let device_id = reader.fixed_bytes::<16>()?; + let signing_key = reader.fixed_bytes::<32>()?; + let encryption_key = reader.fixed_bytes::<32>()?; + let status = match reader.argument(0)? { + 0 => DeviceStatus::Active, + 1 => DeviceStatus::Revoked, + _ => return Err(PolicyError::Malformed), + }; + devices.push(DeviceEntry { + device_id, + signing_key, + encryption_key, + status, + }); + } + reader.finish()?; + Ok(Self { + sequence, + parent_hash, + profile_hash, + epoch, + operation, + recovery_signing_key, + recovery_encryption_key, + devices, + }) + } +} + +/// SHA-256 binding of the account profile without publishing its labels. +pub fn profile_hash(org_id: &str, user_id: &str) -> [u8; 32] { + sha256(&[PROFILE_DOMAIN, org_id.as_bytes(), b"\0", user_id.as_bytes()]) +} + +/// Membership hash of a complete signed policy record. +pub fn membership_hash(encoded_record: &[u8]) -> [u8; 32] { + sha256(&[MEMBERSHIP_DOMAIN, encoded_record]) +} + +/// The author ID under which the recovery authority signs (RFC §5): derived +/// from its public signing key so it needs no separate registration. +pub fn recovery_authority_id(recovery_signing_key: &[u8; 32]) -> [u8; 16] { + let digest = sha256(&[RECOVERY_ID_DOMAIN, recovery_signing_key]); + let mut id = [0; 16]; + id.copy_from_slice(&digest[..16]); + id +} + +/// An enrollment request: the pending device's public identity, bound to +/// the vault it wants to join. The proof is an ordinary Ed25519 signature by +/// the device's signing key over [`enrollment_proof_input`]; it proves key +/// possession to the bootstrap service, never membership. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EnrollmentRequest { + pub vault_id: [u8; 16], + pub request_id: [u8; 16], + pub device_id: [u8; 16], + pub signing_key: [u8; 32], + pub encryption_key: [u8; 32], +} + +impl EnrollmentRequest { + pub fn proof_input(&self) -> Vec { + let mut out = Vec::with_capacity(ENROLL_DOMAIN.len() + 112); + out.extend_from_slice(ENROLL_DOMAIN); + out.extend_from_slice(&self.vault_id); + out.extend_from_slice(&self.request_id); + out.extend_from_slice(&self.device_id); + out.extend_from_slice(&self.signing_key); + out.extend_from_slice(&self.encryption_key); + out + } + + pub fn sign(&self, signer: &DeviceSigner) -> Result<[u8; 64], PolicyError> { + if *signer.author_id() != self.device_id + || signer.public_key() != self.signing_key.as_slice() + { + return Err(PolicyError::UnknownAuthor); + } + Ok(signer.sign_bytes(&self.proof_input())?) + } + + pub fn verify(&self, proof: &[u8]) -> Result<(), PolicyError> { + if !crate::ed25519_point_encoding_precheck(&self.signing_key) + || self.encryption_key.iter().all(|byte| *byte == 0) + || self.device_id == POLICY_OBJECT_ID + { + return Err(PolicyError::InvalidDeviceSet); + } + crate::verify_ed25519(&self.signing_key, &self.proof_input(), proof)?; + Ok(()) + } + + /// The human-comparison code (RFC §6.2): both the pending device and the + /// approving device derive it from the request they each hold plus the + /// genesis hash of the vault they each see, so a relay that substitutes + /// keys OR presents a different vault produces a mismatch the user can + /// see. Eight decimal digits as "NNNN-NNNN". + pub fn pairing_code(&self, genesis_hash: &[u8; 32]) -> String { + let digest = sha256(&[ + PAIRING_DOMAIN, + &self.proof_input()[ENROLL_DOMAIN.len()..], + genesis_hash, + ]); + let value = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]) % 100_000_000; + format!("{:04}-{:04}", value / 10_000, value % 10_000) + } + + pub fn device_entry(&self) -> DeviceEntry { + DeviceEntry { + device_id: self.device_id, + signing_key: self.signing_key, + encryption_key: self.encryption_key, + status: DeviceStatus::Active, + } + } +} + +/// Wrapper binding for a policy record at `epoch` authored by `author_id` +/// under parent hash `parent`. +pub fn policy_binding( + vault_id: [u8; 16], + generation: [u8; 16], + epoch: u64, + author_id: [u8; 16], + parent: [u8; 32], +) -> RecordBinding { + RecordBinding { + kind: RecordKind::Policy, + vault_id, + generation, + epoch, + object_id: POLICY_OBJECT_ID, + author_id, + membership_hash: parent, + } +} + +/// Sign and encode a policy record with a fresh revision ID. The caller is +/// responsible for the payload's meaning; validity is decided on apply. +pub fn encode_policy( + binding: &RecordBinding, + payload: &PolicyPayload, + signer: &DeviceSigner, +) -> Result, PolicyError> { + if binding.kind != RecordKind::Policy || binding.author_id != *signer.author_id() { + return Err(PolicyError::UnknownAuthor); + } + let payload = payload.encode()?; + let mut revision_id = [0; 16]; + crate::fill_random(&mut revision_id)?; + let input = record::signing_bytes(binding, &revision_id, &payload, MAX_POLICY_BYTES)?; + let signature = signer.sign_bytes(&input)?; + Ok(record::encode_signed( + binding, + &revision_id, + &payload, + &signature, + MAX_POLICY_BYTES, + )?) +} + +/// A verified membership head: the trust anchor for every other record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MembershipState { + vault_id: [u8; 16], + generation: [u8; 16], + genesis_hash: [u8; 32], + hash: [u8; 32], + sequence: u64, + epoch: u64, + profile_hash: [u8; 32], + recovery_signing_key: [u8; 32], + recovery_encryption_key: [u8; 32], + devices: Vec, +} + +impl MembershipState { + /// Pin a genesis record. `expected_*` come from local trusted state (the + /// creating device, a pairing transcript, or the recovery kit), never from + /// the server's descriptor alone. + pub fn from_genesis( + encoded: &[u8], + expected_vault_id: &[u8; 16], + expected_generation: &[u8; 16], + expected_profile_hash: &[u8; 32], + ) -> Result { + let parsed = UnverifiedRecord::parse(encoded, MAX_POLICY_BYTES)?; + let untrusted = *parsed.untrusted_binding(); + if untrusted.kind != RecordKind::Policy + || untrusted.vault_id != *expected_vault_id + || untrusted.generation != *expected_generation + || untrusted.object_id != POLICY_OBJECT_ID + { + return Err(PolicyError::WrongVault); + } + let payload = PolicyPayload::decode(payload_of(encoded)?)?; + if payload.operation != Operation::Genesis { + return Err(PolicyError::InvalidTransition); + } + if payload.sequence != 0 || payload.parent_hash != [0; 32] { + return Err(PolicyError::WrongSequence); + } + if payload.profile_hash != *expected_profile_hash { + return Err(PolicyError::WrongProfile); + } + if payload.epoch != 1 { + return Err(PolicyError::WrongEpoch); + } + check_device_entries(&payload.devices)?; + check_recovery_keys(&payload)?; + let [device] = payload.devices.as_slice() else { + return Err(PolicyError::InvalidDeviceSet); + }; + if device.status != DeviceStatus::Active { + return Err(PolicyError::InvalidDeviceSet); + } + let expected = policy_binding( + *expected_vault_id, + *expected_generation, + 1, + device.device_id, + [0; 32], + ); + parsed.verify(&expected, &device.signing_key)?; + Ok(Self { + vault_id: *expected_vault_id, + generation: *expected_generation, + genesis_hash: membership_hash(encoded), + hash: membership_hash(encoded), + sequence: 0, + epoch: 1, + profile_hash: payload.profile_hash, + recovery_signing_key: payload.recovery_signing_key, + recovery_encryption_key: payload.recovery_encryption_key, + devices: payload.devices, + }) + } + + /// Verify and apply the next record in the history. + pub fn apply(&self, encoded: &[u8]) -> Result { + let parsed = UnverifiedRecord::parse(encoded, MAX_POLICY_BYTES)?; + let untrusted = *parsed.untrusted_binding(); + if untrusted.kind != RecordKind::Policy + || untrusted.vault_id != self.vault_id + || untrusted.generation != self.generation + || untrusted.object_id != POLICY_OBJECT_ID + { + return Err(PolicyError::WrongVault); + } + if untrusted.membership_hash != self.hash { + return Err(PolicyError::WrongParent); + } + let payload = PolicyPayload::decode(payload_of(encoded)?)?; + if payload.sequence + != self + .sequence + .checked_add(1) + .ok_or(PolicyError::WrongSequence)? + { + return Err(PolicyError::WrongSequence); + } + if payload.parent_hash != self.hash { + return Err(PolicyError::WrongParent); + } + if payload.profile_hash != self.profile_hash { + return Err(PolicyError::WrongProfile); + } + check_device_entries(&payload.devices)?; + check_recovery_keys(&payload)?; + let next_epoch = self.epoch.checked_add(1).ok_or(PolicyError::WrongEpoch)?; + let signing_key = match payload.operation { + Operation::Genesis => return Err(PolicyError::InvalidTransition), + Operation::RecoveryTransition => { + if untrusted.author_id != recovery_authority_id(&self.recovery_signing_key) { + return Err(PolicyError::UnknownAuthor); + } + self.recovery_signing_key + } + _ => match self.device(&untrusted.author_id) { + Some(device) if device.status == DeviceStatus::Active => device.signing_key, + Some(_) => return Err(PolicyError::RevokedAuthor), + None => return Err(PolicyError::UnknownAuthor), + }, + }; + let expected_epoch = match payload.operation { + Operation::AddDevice => self.epoch, + _ => next_epoch, + }; + if payload.epoch != expected_epoch || untrusted.epoch != expected_epoch { + return Err(PolicyError::WrongEpoch); + } + self.check_transition(&payload)?; + let expected = policy_binding( + self.vault_id, + self.generation, + expected_epoch, + untrusted.author_id, + self.hash, + ); + parsed.verify(&expected, &signing_key)?; + Ok(Self { + vault_id: self.vault_id, + generation: self.generation, + genesis_hash: self.genesis_hash, + hash: membership_hash(encoded), + sequence: payload.sequence, + epoch: payload.epoch, + profile_hash: self.profile_hash, + recovery_signing_key: payload.recovery_signing_key, + recovery_encryption_key: payload.recovery_encryption_key, + devices: payload.devices, + }) + } + + fn check_transition(&self, payload: &PolicyPayload) -> Result<(), PolicyError> { + let recovery_unchanged = payload.recovery_signing_key == self.recovery_signing_key + && payload.recovery_encryption_key == self.recovery_encryption_key; + let recovery_replaced = payload.recovery_signing_key != self.recovery_signing_key + && payload.recovery_encryption_key != self.recovery_encryption_key; + match payload.operation { + Operation::Genesis => Err(PolicyError::InvalidTransition), + Operation::AddDevice => { + if !recovery_unchanged { + return Err(PolicyError::InvalidRecoveryKeys); + } + self.expect_prefix(payload, |previous, next| previous == next)?; + match &payload.devices[self.devices.len()..] { + [added] if added.status == DeviceStatus::Active => Ok(()), + _ => Err(PolicyError::InvalidDeviceSet), + } + } + Operation::RevokeDevice => { + if !recovery_unchanged { + return Err(PolicyError::InvalidRecoveryKeys); + } + if payload.devices.len() != self.devices.len() { + return Err(PolicyError::InvalidDeviceSet); + } + let mut revoked = 0; + self.expect_prefix(payload, |previous, next| { + if previous == next { + return true; + } + let keys_match = previous.device_id == next.device_id + && previous.signing_key == next.signing_key + && previous.encryption_key == next.encryption_key; + let newly_revoked = previous.status == DeviceStatus::Active + && next.status == DeviceStatus::Revoked; + if keys_match && newly_revoked { + revoked += 1; + true + } else { + false + } + })?; + if revoked == 1 { + Ok(()) + } else { + Err(PolicyError::InvalidDeviceSet) + } + } + Operation::RotateRecovery => { + if !recovery_replaced { + return Err(PolicyError::InvalidRecoveryKeys); + } + if payload.devices != self.devices { + return Err(PolicyError::InvalidDeviceSet); + } + Ok(()) + } + Operation::RecoveryTransition => { + if !(recovery_unchanged || recovery_replaced) { + return Err(PolicyError::InvalidRecoveryKeys); + } + self.expect_prefix(payload, |previous, next| { + previous == next + || (previous.device_id == next.device_id + && previous.signing_key == next.signing_key + && previous.encryption_key == next.encryption_key + && next.status == DeviceStatus::Revoked) + })?; + match &payload.devices[self.devices.len()..] { + [added] if added.status == DeviceStatus::Active => Ok(()), + _ => Err(PolicyError::InvalidDeviceSet), + } + } + } + } + + /// Every existing device must appear, in order, at the same index. + fn expect_prefix( + &self, + payload: &PolicyPayload, + mut accept: impl FnMut(&DeviceEntry, &DeviceEntry) -> bool, + ) -> Result<(), PolicyError> { + if payload.devices.len() < self.devices.len() { + return Err(PolicyError::InvalidDeviceSet); + } + for (previous, next) in self.devices.iter().zip(&payload.devices) { + if !accept(previous, next) { + return Err(PolicyError::InvalidDeviceSet); + } + } + Ok(()) + } + + pub fn vault_id(&self) -> &[u8; 16] { + &self.vault_id + } + pub fn generation(&self) -> &[u8; 16] { + &self.generation + } + pub fn genesis_hash(&self) -> &[u8; 32] { + &self.genesis_hash + } + pub fn hash(&self) -> &[u8; 32] { + &self.hash + } + pub fn sequence(&self) -> u64 { + self.sequence + } + pub fn epoch(&self) -> u64 { + self.epoch + } + pub fn profile_hash(&self) -> &[u8; 32] { + &self.profile_hash + } + pub fn recovery_signing_key(&self) -> &[u8; 32] { + &self.recovery_signing_key + } + pub fn recovery_encryption_key(&self) -> &[u8; 32] { + &self.recovery_encryption_key + } + pub fn recovery_authority_id(&self) -> [u8; 16] { + recovery_authority_id(&self.recovery_signing_key) + } + pub fn devices(&self) -> &[DeviceEntry] { + &self.devices + } + pub fn device(&self, device_id: &[u8; 16]) -> Option<&DeviceEntry> { + self.devices + .iter() + .find(|device| device.device_id == *device_id) + } + pub fn active_device(&self, device_id: &[u8; 16]) -> Option<&DeviceEntry> { + self.device(device_id) + .filter(|device| device.status == DeviceStatus::Active) + } + + /// The payload a successor record must carry before its own changes. + pub fn next_payload(&self, operation: Operation) -> PolicyPayload { + PolicyPayload { + sequence: self.sequence.saturating_add(1), + parent_hash: self.hash, + profile_hash: self.profile_hash, + epoch: match operation { + Operation::AddDevice | Operation::Genesis => self.epoch, + _ => self.epoch.saturating_add(1), + }, + operation, + recovery_signing_key: self.recovery_signing_key, + recovery_encryption_key: self.recovery_encryption_key, + devices: self.devices.clone(), + } + } + + /// Binding for a content record written to `object_id` by `author_id` + /// under the current epoch and this membership. + pub fn content_binding(&self, object_id: [u8; 16], author_id: [u8; 16]) -> RecordBinding { + RecordBinding { + kind: RecordKind::Content, + vault_id: self.vault_id, + generation: self.generation, + epoch: self.epoch, + object_id, + author_id, + membership_hash: self.hash, + } + } + + /// Binding for a key envelope authored by `author_id` for `object_id` + /// (the policy object for keyrings) at `epoch`. + pub fn envelope_binding( + &self, + object_id: [u8; 16], + epoch: u64, + author_id: [u8; 16], + ) -> RecordBinding { + RecordBinding { + kind: RecordKind::KeyEnvelope, + vault_id: self.vault_id, + generation: self.generation, + epoch, + object_id, + author_id, + membership_hash: self.hash, + } + } +} + +fn payload_of(encoded: &[u8]) -> Result<&[u8], PolicyError> { + // The wrapper is re-parsed here only to borrow its payload; the caller + // has already bounded and parsed it once. + let parsed = UnverifiedRecord::parse(encoded, MAX_POLICY_BYTES)?; + Ok(parsed.untrusted_payload()) +} + +fn check_device_entries(devices: &[DeviceEntry]) -> Result<(), PolicyError> { + if devices.is_empty() || devices.len() > MAX_DEVICES { + return Err(PolicyError::InvalidDeviceSet); + } + for (index, device) in devices.iter().enumerate() { + if !crate::ed25519_point_encoding_precheck(&device.signing_key) + || device.encryption_key.iter().all(|byte| *byte == 0) + || device.device_id == POLICY_OBJECT_ID + { + return Err(PolicyError::InvalidDeviceSet); + } + if devices[..index].iter().any(|other| { + other.device_id == device.device_id + || other.signing_key == device.signing_key + || other.encryption_key == device.encryption_key + }) { + return Err(PolicyError::InvalidDeviceSet); + } + } + Ok(()) +} + +fn check_recovery_keys(payload: &PolicyPayload) -> Result<(), PolicyError> { + if !crate::ed25519_point_encoding_precheck(&payload.recovery_signing_key) + || payload + .recovery_encryption_key + .iter() + .all(|byte| *byte == 0) + { + return Err(PolicyError::InvalidRecoveryKeys); + } + if payload.devices.iter().any(|device| { + device.signing_key == payload.recovery_signing_key + || device.encryption_key == payload.recovery_encryption_key + }) { + return Err(PolicyError::InvalidRecoveryKeys); + } + Ok(()) +} + +#[cfg(test)] +#[path = "policy_tests.rs"] +mod tests; diff --git a/crates/crypto/src/policy_tests.rs b/crates/crypto/src/policy_tests.rs new file mode 100644 index 000000000..cf39fee77 --- /dev/null +++ b/crates/crypto/src/policy_tests.rs @@ -0,0 +1,416 @@ +use super::*; +use crate::content::DeviceSigner; +use crate::hpke::HpkePrivateKey; +use crate::recovery::RecoverySecret; + +struct Device { + id: [u8; 16], + signer: DeviceSigner, + encryption: HpkePrivateKey, +} + +impl Device { + fn new(tag: u8) -> Self { + let mut seed = [tag; 32]; + seed[0] ^= 0x5a; + Self { + id: [tag; 16], + signer: DeviceSigner::from_seed([tag; 16], &seed).unwrap(), + encryption: HpkePrivateKey::from_bytes(&[tag ^ 0x33; 32]).unwrap(), + } + } + + fn entry(&self, status: DeviceStatus) -> DeviceEntry { + DeviceEntry { + device_id: self.id, + signing_key: self.signer.public_key().try_into().unwrap(), + encryption_key: *self.encryption.public_key().as_bytes(), + status, + } + } +} + +const VAULT: [u8; 16] = [1; 16]; +const GENERATION: [u8; 16] = [2; 16]; + +fn recovery() -> RecoverySecret { + RecoverySecret::from_bytes(&[77; 32]).unwrap() +} + +fn genesis_payload(device: &Device, recovery: &RecoverySecret) -> PolicyPayload { + PolicyPayload { + sequence: 0, + parent_hash: [0; 32], + profile_hash: profile_hash("org", "user"), + epoch: 1, + operation: Operation::Genesis, + recovery_signing_key: recovery.signing_public_key().unwrap(), + recovery_encryption_key: *recovery.encryption_key().unwrap().public_key().as_bytes(), + devices: vec![device.entry(DeviceStatus::Active)], + } +} + +fn genesis(device: &Device) -> (Vec, MembershipState) { + let recovery = recovery(); + let payload = genesis_payload(device, &recovery); + let binding = policy_binding(VAULT, GENERATION, 1, device.id, [0; 32]); + let encoded = encode_policy(&binding, &payload, &device.signer).unwrap(); + let state = + MembershipState::from_genesis(&encoded, &VAULT, &GENERATION, &profile_hash("org", "user")) + .unwrap(); + (encoded, state) +} + +fn sign_next( + state: &MembershipState, + payload: &PolicyPayload, + author: [u8; 16], + signer: &DeviceSigner, +) -> Vec { + let binding = policy_binding(VAULT, GENERATION, payload.epoch, author, *state.hash()); + encode_policy(&binding, payload, signer).unwrap() +} + +#[test] +fn genesis_pins_the_initial_device_and_rejects_wrong_expectations() { + let device = Device::new(10); + let (encoded, state) = genesis(&device); + assert_eq!(state.sequence(), 0); + assert_eq!(state.epoch(), 1); + assert_eq!(state.hash(), &membership_hash(&encoded)); + assert_eq!(state.genesis_hash(), state.hash()); + assert!(state.active_device(&device.id).is_some()); + assert_eq!( + state.recovery_authority_id(), + recovery_authority_id(&recovery().signing_public_key().unwrap()) + ); + let profile = profile_hash("org", "user"); + assert!(matches!( + MembershipState::from_genesis(&encoded, &[9; 16], &GENERATION, &profile), + Err(PolicyError::WrongVault) + )); + assert!(matches!( + MembershipState::from_genesis(&encoded, &VAULT, &[9; 16], &profile), + Err(PolicyError::WrongVault) + )); + assert!(matches!( + MembershipState::from_genesis(&encoded, &VAULT, &GENERATION, &profile_hash("org", "x")), + Err(PolicyError::WrongProfile) + )); + // Every single-byte mutation of the record fails closed. + for index in 0..encoded.len() { + let mut damaged = encoded.clone(); + damaged[index] ^= 0x01; + assert!( + MembershipState::from_genesis(&damaged, &VAULT, &GENERATION, &profile).is_err(), + "byte {index} mutation accepted" + ); + } + // A genesis cannot be applied on top of itself. + assert!(matches!( + state.apply(&encoded), + Err(PolicyError::WrongParent) | Err(PolicyError::WrongSequence) + )); +} + +#[test] +fn genesis_requires_a_self_signed_single_active_device_at_epoch_one() { + let device = Device::new(10); + let other = Device::new(11); + let recovery = recovery(); + let profile = profile_hash("org", "user"); + // Signed by a device that is not the listed member. + let payload = genesis_payload(&device, &recovery); + let binding = policy_binding(VAULT, GENERATION, 1, other.id, [0; 32]); + let encoded = encode_policy(&binding, &payload, &other.signer).unwrap(); + assert!(MembershipState::from_genesis(&encoded, &VAULT, &GENERATION, &profile).is_err()); + // Two devices at genesis. + let mut payload = genesis_payload(&device, &recovery); + payload.devices.push(other.entry(DeviceStatus::Active)); + let binding = policy_binding(VAULT, GENERATION, 1, device.id, [0; 32]); + let encoded = encode_policy(&binding, &payload, &device.signer).unwrap(); + assert!(matches!( + MembershipState::from_genesis(&encoded, &VAULT, &GENERATION, &profile), + Err(PolicyError::InvalidDeviceSet) + )); + // Wrong epoch. + let mut payload = genesis_payload(&device, &recovery); + payload.epoch = 2; + let binding = policy_binding(VAULT, GENERATION, 2, device.id, [0; 32]); + let encoded = encode_policy(&binding, &payload, &device.signer).unwrap(); + assert!(matches!( + MembershipState::from_genesis(&encoded, &VAULT, &GENERATION, &profile), + Err(PolicyError::WrongEpoch) + )); + // Recovery keys may not coincide with a device's keys. + let mut payload = genesis_payload(&device, &recovery); + payload.recovery_signing_key = payload.devices[0].signing_key; + let binding = policy_binding(VAULT, GENERATION, 1, device.id, [0; 32]); + let encoded = encode_policy(&binding, &payload, &device.signer).unwrap(); + assert!(matches!( + MembershipState::from_genesis(&encoded, &VAULT, &GENERATION, &profile), + Err(PolicyError::InvalidRecoveryKeys) + )); +} + +#[test] +fn add_and_revoke_devices_follow_the_transition_rules() { + let first = Device::new(10); + let second = Device::new(11); + let (_, state) = genesis(&first); + + let mut add = state.next_payload(Operation::AddDevice); + add.devices.push(second.entry(DeviceStatus::Active)); + assert_eq!(add.epoch, 1, "adding a device keeps the write epoch"); + let encoded = sign_next(&state, &add, first.id, &first.signer); + let state = state.apply(&encoded).unwrap(); + assert_eq!(state.sequence(), 1); + assert_eq!(state.devices().len(), 2); + assert!(state.active_device(&second.id).is_some()); + + // The new device may now authorize changes; an unknown one may not. + let third = Device::new(12); + let mut add_third = state.next_payload(Operation::AddDevice); + add_third.devices.push(third.entry(DeviceStatus::Active)); + let by_second = sign_next(&state, &add_third, second.id, &second.signer); + let state_with_third = state.apply(&by_second).unwrap(); + let by_third = sign_next(&state, &add_third, third.id, &third.signer); + assert!(matches!( + state.apply(&by_third), + Err(PolicyError::UnknownAuthor) + )); + + // Revocation must bump the epoch and flip exactly one device. + let mut revoke = state_with_third.next_payload(Operation::RevokeDevice); + revoke.devices[2].status = DeviceStatus::Revoked; + assert_eq!(revoke.epoch, 2); + let encoded = sign_next(&state_with_third, &revoke, first.id, &first.signer); + let revoked = state_with_third.apply(&encoded).unwrap(); + assert_eq!(revoked.epoch(), 2); + assert!(revoked.active_device(&third.id).is_none()); + assert!( + revoked.device(&third.id).is_some(), + "history keeps the entry" + ); + + // The revoked device cannot sign further changes. + let mut readd = revoked.next_payload(Operation::AddDevice); + readd + .devices + .push(Device::new(13).entry(DeviceStatus::Active)); + let encoded = sign_next(&revoked, &readd, third.id, &third.signer); + assert!(matches!( + revoked.apply(&encoded), + Err(PolicyError::RevokedAuthor) + )); + // Nor can anyone revive it. + let mut revive = revoked.next_payload(Operation::AddDevice); + revive.devices[2].status = DeviceStatus::Active; + let encoded = sign_next(&revoked, &revive, first.id, &first.signer); + assert!(matches!( + revoked.apply(&encoded), + Err(PolicyError::InvalidDeviceSet) + )); + // A revoke that forgets the epoch bump is rejected. + let mut lazy = revoked.next_payload(Operation::RevokeDevice); + lazy.devices[1].status = DeviceStatus::Revoked; + lazy.epoch = revoked.epoch(); + let encoded = sign_next(&revoked, &lazy, first.id, &first.signer); + assert!(matches!( + revoked.apply(&encoded), + Err(PolicyError::WrongEpoch) + )); + // An add that also touches the recovery keys is rejected. + let mut sneaky = revoked.next_payload(Operation::AddDevice); + sneaky + .devices + .push(Device::new(14).entry(DeviceStatus::Active)); + sneaky.recovery_signing_key = Device::new(15).entry(DeviceStatus::Active).signing_key; + let encoded = sign_next(&revoked, &sneaky, first.id, &first.signer); + assert!(matches!( + revoked.apply(&encoded), + Err(PolicyError::InvalidRecoveryKeys) + )); +} + +#[test] +fn history_links_reject_forks_replays_and_stale_parents() { + let first = Device::new(10); + let second = Device::new(11); + let (_, state) = genesis(&first); + let mut add = state.next_payload(Operation::AddDevice); + add.devices.push(second.entry(DeviceStatus::Active)); + let encoded = sign_next(&state, &add, first.id, &first.signer); + let next = state.apply(&encoded).unwrap(); + // Replaying the same record on the new head fails (parent moved). + assert!(matches!( + next.apply(&encoded), + Err(PolicyError::WrongParent) + )); + // A concurrent record built on the OLD head is rejected by the new head. + let mut fork = state.next_payload(Operation::AddDevice); + fork.devices + .push(Device::new(12).entry(DeviceStatus::Active)); + let fork_encoded = sign_next(&state, &fork, first.id, &first.signer); + assert!(matches!( + next.apply(&fork_encoded), + Err(PolicyError::WrongParent) + )); + // A record whose payload claims the right parent but whose wrapper + // membership hash is stale is rejected before signature checks. + let mut skip = next.next_payload(Operation::AddDevice); + skip.devices + .push(Device::new(12).entry(DeviceStatus::Active)); + skip.sequence += 1; + let encoded = sign_next(&next, &skip, first.id, &first.signer); + assert!(matches!( + next.apply(&encoded), + Err(PolicyError::WrongSequence) + )); + // Wrong vault in the wrapper. + let good = { + let mut payload = next.next_payload(Operation::AddDevice); + payload + .devices + .push(Device::new(12).entry(DeviceStatus::Active)); + payload + }; + let binding = policy_binding([9; 16], GENERATION, 1, first.id, *next.hash()); + let encoded = encode_policy(&binding, &good, &first.signer).unwrap(); + assert!(matches!(next.apply(&encoded), Err(PolicyError::WrongVault))); +} + +#[test] +fn recovery_authority_can_transition_and_rotate() { + let first = Device::new(10); + let (_, state) = genesis(&first); + let recovery = recovery(); + let replacement = Device::new(20); + + // Recovery transition: authored by the recovery authority, revokes the + // lost device and enrolls the replacement under a fresh epoch. + let mut transition = state.next_payload(Operation::RecoveryTransition); + transition.devices[0].status = DeviceStatus::Revoked; + transition + .devices + .push(replacement.entry(DeviceStatus::Active)); + assert_eq!(transition.epoch, 2); + let signer = recovery.signer().unwrap(); + let encoded = sign_next(&state, &transition, *signer.author_id(), &signer); + let recovered = state.apply(&encoded).unwrap(); + assert_eq!(recovered.epoch(), 2); + assert!(recovered.active_device(&replacement.id).is_some()); + assert!(recovered.active_device(&first.id).is_none()); + + // A device cannot author a recovery transition. + let encoded = sign_next(&state, &transition, first.id, &first.signer); + assert!(matches!( + state.apply(&encoded), + Err(PolicyError::UnknownAuthor) + )); + // The wrong recovery secret cannot either. + let wrong = RecoverySecret::from_bytes(&[78; 32]) + .unwrap() + .signer() + .unwrap(); + let encoded = sign_next(&state, &transition, *wrong.author_id(), &wrong); + assert!(matches!( + state.apply(&encoded), + Err(PolicyError::UnknownAuthor) + )); + + // Rotate the recovery authority from a trusted device. + let new_recovery = RecoverySecret::from_bytes(&[79; 32]).unwrap(); + let mut rotate = recovered.next_payload(Operation::RotateRecovery); + rotate.recovery_signing_key = new_recovery.signing_public_key().unwrap(); + rotate.recovery_encryption_key = *new_recovery + .encryption_key() + .unwrap() + .public_key() + .as_bytes(); + let encoded = sign_next(&recovered, &rotate, replacement.id, &replacement.signer); + let rotated = recovered.apply(&encoded).unwrap(); + assert_eq!(rotated.epoch(), 3); + assert_eq!( + rotated.recovery_authority_id(), + *new_recovery.signer().unwrap().author_id() + ); + // The old authority is no longer honored. + let mut late = rotated.next_payload(Operation::RecoveryTransition); + late.devices + .push(Device::new(21).entry(DeviceStatus::Active)); + let encoded = sign_next(&rotated, &late, *signer.author_id(), &signer); + assert!(matches!( + rotated.apply(&encoded), + Err(PolicyError::UnknownAuthor) + )); + // Half-rotating the recovery keys is not a rotation. + let mut half = rotated.next_payload(Operation::RotateRecovery); + half.recovery_signing_key = recovery.signing_public_key().unwrap(); + let encoded = sign_next(&rotated, &half, replacement.id, &replacement.signer); + assert!(matches!( + rotated.apply(&encoded), + Err(PolicyError::InvalidRecoveryKeys) + )); +} + +#[test] +fn payload_codec_round_trips_and_rejects_malformed() { + let device = Device::new(10); + let payload = genesis_payload(&device, &recovery()); + let encoded = payload.encode().unwrap(); + assert_eq!(PolicyPayload::decode(&encoded).unwrap(), payload); + assert!(PolicyPayload::decode(&encoded[..encoded.len() - 1]).is_err()); + let mut trailing = encoded.clone(); + trailing.push(0); + assert!(PolicyPayload::decode(&trailing).is_err()); + let mut version = encoded.clone(); + version[2] = 2; + assert!(matches!( + PolicyPayload::decode(&version), + Err(PolicyError::UnsupportedVersion) + )); + let mut too_many = payload.clone(); + too_many.devices = (0..MAX_DEVICES as u8 + 1) + .map(|tag| Device::new(tag.wrapping_add(30)).entry(DeviceStatus::Active)) + .collect(); + assert!(matches!( + too_many.encode(), + Err(PolicyError::TooManyDevices) + )); + assert_ne!(profile_hash("a", "b"), profile_hash("ab", "")); +} + +#[test] +fn enrollment_proofs_bind_keys_and_pairing_codes_agree() { + let device = Device::new(40); + let request = EnrollmentRequest { + vault_id: VAULT, + request_id: [3; 16], + device_id: device.id, + signing_key: device.signer.public_key().try_into().unwrap(), + encryption_key: *device.encryption.public_key().as_bytes(), + }; + let proof = request.sign(&device.signer).unwrap(); + request.verify(&proof).unwrap(); + let mut substituted = request; + substituted.encryption_key = [9; 32]; + assert!(substituted.verify(&proof).is_err()); + assert_ne!( + substituted.pairing_code(&[1; 32]), + request.pairing_code(&[1; 32]) + ); + assert_ne!( + request.pairing_code(&[2; 32]), + request.pairing_code(&[1; 32]) + ); + let code = request.pairing_code(&[1; 32]); + assert_eq!(code.len(), 9); + assert!(code[..4].bytes().all(|b| b.is_ascii_digit())); + assert_eq!(&code[4..5], "-"); + let other = Device::new(41); + assert!(matches!( + request.sign(&other.signer), + Err(PolicyError::UnknownAuthor) + )); +} diff --git a/crates/crypto/src/record.rs b/crates/crypto/src/record.rs new file mode 100644 index 000000000..f1a93a9ac --- /dev/null +++ b/crates/crypto/src/record.rs @@ -0,0 +1,375 @@ +use crate::verify_ed25519; +use std::fmt; + +const DOMAIN: &[u8] = b"zeron/signed-record/v1\0"; +const MAX_OVERHEAD: usize = 256; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RecordError { + Malformed, + NonCanonical, + UnsupportedVersion, + UnsupportedKind, + InvalidEpoch, + SizeLimitExceeded, + ContextMismatch, + InvalidSignature, +} + +impl fmt::Display for RecordError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for RecordError {} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum RecordKind { + Policy = 1, + KeyEnvelope = 2, + Content = 3, +} + +impl TryFrom for RecordKind { + type Error = RecordError; + + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(Self::Policy), + 2 => Ok(Self::KeyEnvelope), + 3 => Ok(Self::Content), + _ => Err(RecordError::UnsupportedKind), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RecordBinding { + pub kind: RecordKind, + pub vault_id: [u8; 16], + pub generation: [u8; 16], + pub epoch: u64, + pub object_id: [u8; 16], + pub author_id: [u8; 16], + pub membership_hash: [u8; 32], +} + +pub struct UnverifiedRecord<'a> { + binding: RecordBinding, + revision_id: [u8; 16], + payload: &'a [u8], + signature: [u8; 64], + max_payload_bytes: usize, +} + +impl<'a> UnverifiedRecord<'a> { + pub fn parse(encoded: &'a [u8], max_payload_bytes: usize) -> Result { + if encoded.len() > total_limit(max_payload_bytes)? { + return Err(RecordError::SizeLimitExceeded); + } + let mut reader = Reader(encoded); + if reader.argument(5)? != 11 { + return Err(RecordError::Malformed); + } + if reader.uint_field(0)? != 1 { + return Err(RecordError::UnsupportedVersion); + } + let kind = RecordKind::try_from(reader.uint_field(1)?)?; + let vault_id = reader.fixed_field(2)?; + let generation = reader.fixed_field(3)?; + let epoch = reader.uint_field(4)?; + if epoch == 0 { + return Err(RecordError::InvalidEpoch); + } + let object_id = reader.fixed_field(5)?; + let author_id = reader.fixed_field(6)?; + let revision_id = reader.fixed_field(7)?; + let membership_hash = reader.fixed_field(8)?; + let payload = reader.bytes_field(9, max_payload_bytes)?; + let signature = reader.fixed_field(10)?; + if !reader.0.is_empty() { + return Err(RecordError::Malformed); + } + Ok(Self { + binding: RecordBinding { + kind, + vault_id, + generation, + epoch, + object_id, + author_id, + membership_hash, + }, + revision_id, + payload, + signature, + max_payload_bytes, + }) + } + + pub fn untrusted_binding(&self) -> &RecordBinding { + &self.binding + } + + pub fn untrusted_revision_id(&self) -> &[u8; 16] { + &self.revision_id + } + + /// The payload bytes BEFORE verification — for decoding public routing + /// fields only (a policy record's device list must be read to find the + /// key that verifies it). Nothing decoded from here is trusted until + /// [`Self::verify`] succeeds against an independently expected binding. + pub fn untrusted_payload(&self) -> &'a [u8] { + self.payload + } + + pub fn verify( + self, + expected: &RecordBinding, + trusted_public_key: &[u8], + ) -> Result, RecordError> { + if &self.binding != expected { + return Err(RecordError::ContextMismatch); + } + let input = signing_bytes( + &self.binding, + &self.revision_id, + self.payload, + self.max_payload_bytes, + )?; + verify_ed25519(trusted_public_key, &input, &self.signature) + .map_err(|_| RecordError::InvalidSignature)?; + Ok(VerifiedRecord { + binding: self.binding, + revision_id: self.revision_id, + payload: self.payload, + }) + } +} + +impl fmt::Debug for UnverifiedRecord<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("UnverifiedRecord([REDACTED])") + } +} + +pub struct VerifiedRecord<'a> { + binding: RecordBinding, + revision_id: [u8; 16], + payload: &'a [u8], +} + +impl VerifiedRecord<'_> { + pub fn binding(&self) -> &RecordBinding { + &self.binding + } + + pub fn revision_id(&self) -> &[u8; 16] { + &self.revision_id + } + + pub fn payload(&self) -> &[u8] { + self.payload + } +} + +impl fmt::Debug for VerifiedRecord<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("VerifiedRecord([REDACTED])") + } +} + +pub fn signing_bytes( + binding: &RecordBinding, + revision_id: &[u8; 16], + payload: &[u8], + max_payload_bytes: usize, +) -> Result, RecordError> { + let mut out = buffer(binding, payload, max_payload_bytes)?; + out.extend_from_slice(DOMAIN); + fields(&mut out, 10, binding, revision_id, payload); + Ok(out) +} + +pub fn encode_signed( + binding: &RecordBinding, + revision_id: &[u8; 16], + payload: &[u8], + signature: &[u8; 64], + max_payload_bytes: usize, +) -> Result, RecordError> { + let mut out = buffer(binding, payload, max_payload_bytes)?; + fields(&mut out, 11, binding, revision_id, payload); + bytes_field(&mut out, 10, signature); + Ok(out) +} + +fn total_limit(payload_limit: usize) -> Result { + payload_limit + .checked_add(MAX_OVERHEAD) + .ok_or(RecordError::SizeLimitExceeded) +} + +fn buffer( + binding: &RecordBinding, + payload: &[u8], + max_payload_bytes: usize, +) -> Result, RecordError> { + total_limit(max_payload_bytes)?; + if binding.epoch == 0 { + return Err(RecordError::InvalidEpoch); + } + if payload.len() > max_payload_bytes { + return Err(RecordError::SizeLimitExceeded); + } + Ok(Vec::with_capacity(payload.len() + MAX_OVERHEAD)) +} + +pub(crate) fn context_bytes( + binding: &RecordBinding, + revision_id: &[u8; 16], +) -> Result, RecordError> { + let mut output = buffer(binding, &[], 0)?; + header_fields(&mut output, 9, binding, revision_id); + Ok(output) +} + +fn fields( + out: &mut Vec, + count: u64, + binding: &RecordBinding, + revision_id: &[u8; 16], + payload: &[u8], +) { + header_fields(out, count, binding, revision_id); + bytes_field(out, 9, payload); +} + +fn header_fields(out: &mut Vec, count: u64, binding: &RecordBinding, revision_id: &[u8; 16]) { + argument(out, 5, count); + uint_field(out, 0, 1); + uint_field(out, 1, binding.kind as u64); + bytes_field(out, 2, &binding.vault_id); + bytes_field(out, 3, &binding.generation); + uint_field(out, 4, binding.epoch); + bytes_field(out, 5, &binding.object_id); + bytes_field(out, 6, &binding.author_id); + bytes_field(out, 7, revision_id); + bytes_field(out, 8, &binding.membership_hash); +} + +pub(crate) fn uint_field(out: &mut Vec, key: u64, value: u64) { + argument(out, 0, key); + argument(out, 0, value); +} + +pub(crate) fn bytes_field(out: &mut Vec, key: u64, value: &[u8]) { + argument(out, 0, key); + argument(out, 2, value.len() as u64); + out.extend_from_slice(value); +} + +pub(crate) fn argument(out: &mut Vec, major: u8, value: u64) { + if value < 24 { + out.push((major << 5) | value as u8); + return; + } + let (width, additional) = match value { + 24..=0xff => (1, 24), + 0x100..=0xffff => (2, 25), + 0x10000..=0xffffffff => (4, 26), + _ => (8, 27), + }; + out.push((major << 5) | additional); + out.extend_from_slice(&value.to_be_bytes()[8 - width..]); +} + +pub(crate) struct Reader<'a>(&'a [u8]); + +impl<'a> Reader<'a> { + pub(crate) fn new(bytes: &'a [u8]) -> Self { + Self(bytes) + } + + pub(crate) fn finish(&self) -> Result<(), RecordError> { + if self.0.is_empty() { + Ok(()) + } else { + Err(RecordError::Malformed) + } + } + + fn take(&mut self, length: usize) -> Result<&'a [u8], RecordError> { + if length > self.0.len() { + return Err(RecordError::Malformed); + } + let (value, rest) = self.0.split_at(length); + self.0 = rest; + Ok(value) + } + + pub(crate) fn argument(&mut self, major: u8) -> Result { + let head = self.take(1)?[0]; + if head >> 5 != major { + return Err(RecordError::Malformed); + } + let (length, minimum) = match head & 31 { + value @ 0..=23 => return Ok(value as u64), + 24 => (1, 24), + 25 => (2, 0x100), + 26 => (4, 0x10000), + 27 => (8, 0x100000000), + _ => return Err(RecordError::Malformed), + }; + let value = self + .take(length)? + .iter() + .fold(0u64, |value, byte| (value << 8) | u64::from(*byte)); + if value < minimum { + return Err(RecordError::NonCanonical); + } + Ok(value) + } + + fn key(&mut self, expected: u64) -> Result<(), RecordError> { + if self.argument(0)? != expected { + return Err(RecordError::Malformed); + } + Ok(()) + } + + pub(crate) fn uint_field(&mut self, key: u64) -> Result { + self.key(key)?; + self.argument(0) + } + + pub(crate) fn bytes_field(&mut self, key: u64, limit: usize) -> Result<&'a [u8], RecordError> { + self.key(key)?; + let length = + usize::try_from(self.argument(2)?).map_err(|_| RecordError::SizeLimitExceeded)?; + if length > limit { + return Err(RecordError::SizeLimitExceeded); + } + self.take(length) + } + + pub(crate) fn fixed_field(&mut self, key: u64) -> Result<[u8; N], RecordError> { + self.bytes_field(key, N)? + .try_into() + .map_err(|_| RecordError::Malformed) + } + + /// A bare byte string of exactly `N` bytes (array elements carry no key). + pub(crate) fn fixed_bytes(&mut self) -> Result<[u8; N], RecordError> { + let length = + usize::try_from(self.argument(2)?).map_err(|_| RecordError::SizeLimitExceeded)?; + if length != N { + return Err(RecordError::Malformed); + } + self.take(length)? + .try_into() + .map_err(|_| RecordError::Malformed) + } +} diff --git a/crates/crypto/src/record_tests.rs b/crates/crypto/src/record_tests.rs new file mode 100644 index 000000000..0e4a7cf28 --- /dev/null +++ b/crates/crypto/src/record_tests.rs @@ -0,0 +1,302 @@ +use crate::record::{ + RecordBinding, RecordError, RecordKind, UnverifiedRecord, encode_signed, signing_bytes, +}; +use crate::tests::hex; +use ring::signature::{Ed25519KeyPair, KeyPair}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct Fixtures { + signed_records: Vec, + record_mutations: Vec, +} + +#[derive(Deserialize)] +struct Mutation { + name: String, + offset: usize, + remove: usize, + insert: String, + error: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct Fixture { + name: String, + kind: u64, + vault_id: String, + generation: String, + epoch: u64, + object_id: String, + author_id: String, + revision_id: String, + membership_hash: String, + payload: String, + seed: String, + public_key: String, + signing_bytes: String, + signature: String, + peer_signature: Option, + peer_record: Option, +} + +fn binding() -> RecordBinding { + RecordBinding { + kind: RecordKind::Content, + vault_id: [1; 16], + generation: [2; 16], + epoch: 24, + object_id: [3; 16], + author_id: [4; 16], + membership_hash: [6; 32], + } +} + +fn signed(binding: &RecordBinding, revision: &[u8; 16], payload: &[u8]) -> (Vec, Vec) { + let signer = Ed25519KeyPair::from_seed_unchecked(&[42; 32]).unwrap(); + let input = signing_bytes(binding, revision, payload, payload.len()).unwrap(); + let signature: [u8; 64] = signer.sign(&input).as_ref().try_into().unwrap(); + ( + encode_signed(binding, revision, payload, &signature, payload.len()).unwrap(), + signer.public_key().as_ref().to_vec(), + ) +} + +#[test] +fn signed_record_rejects_identity_signer() { + let context = binding(); + let mut identity = [0; 32]; + identity[0] = 1; + let mut signature = [0; 64]; + signature[0] = 1; + let encoded = encode_signed(&context, &[5; 16], &[], &signature, 0).unwrap(); + assert_eq!( + UnverifiedRecord::parse(&encoded, 0) + .unwrap() + .verify(&context, &identity) + .unwrap_err(), + RecordError::InvalidSignature + ); +} + +#[test] +fn signed_record_shared_fixture() { + let live = std::env::var("ZERON_CRYPTO_TEST_VECTORS") + .ok() + .map(|path| std::fs::read_to_string(path).unwrap()); + let fixtures: Fixtures = serde_json::from_str( + live.as_deref() + .unwrap_or(include_str!("../tests/fixtures/primitives.json")), + ) + .unwrap(); + assert!(!fixtures.signed_records.is_empty()); + for v in fixtures.signed_records { + let binding = RecordBinding { + kind: RecordKind::try_from(v.kind).unwrap(), + vault_id: hex(&v.vault_id).try_into().unwrap(), + generation: hex(&v.generation).try_into().unwrap(), + epoch: v.epoch, + object_id: hex(&v.object_id).try_into().unwrap(), + author_id: hex(&v.author_id).try_into().unwrap(), + membership_hash: hex(&v.membership_hash).try_into().unwrap(), + }; + let revision: [u8; 16] = hex(&v.revision_id).try_into().unwrap(); + let payload = hex(&v.payload); + let input = signing_bytes(&binding, &revision, &payload, payload.len()).unwrap(); + assert_eq!(input, hex(&v.signing_bytes), "{}", v.name); + let signer = Ed25519KeyPair::from_seed_unchecked(&hex(&v.seed)).unwrap(); + assert_eq!(signer.public_key().as_ref(), hex(&v.public_key)); + let signature: [u8; 64] = signer.sign(&input).as_ref().try_into().unwrap(); + assert_eq!(signature.as_slice(), hex(&v.signature)); + let encoded = + encode_signed(&binding, &revision, &payload, &signature, payload.len()).unwrap(); + let unverified = UnverifiedRecord::parse(&encoded, payload.len()).unwrap(); + assert_eq!(unverified.untrusted_binding(), &binding); + assert_eq!(format!("{unverified:?}"), "UnverifiedRecord([REDACTED])"); + let verified = unverified.verify(&binding, &hex(&v.public_key)).unwrap(); + assert_eq!(verified.payload(), payload); + assert_eq!(verified.binding(), &binding); + assert_eq!(verified.revision_id(), &revision); + assert_eq!(format!("{verified:?}"), "VerifiedRecord([REDACTED])"); + if live.is_some() { + let peer_signature: [u8; 64] = hex(&v.peer_signature.unwrap()).try_into().unwrap(); + let peer_encoded = hex(&v.peer_record.unwrap()); + assert_eq!( + peer_encoded, + encode_signed( + &binding, + &revision, + &payload, + &peer_signature, + payload.len() + ) + .unwrap() + ); + let peer = UnverifiedRecord::parse(&peer_encoded, payload.len()) + .unwrap() + .verify(&binding, &hex(&v.public_key)) + .unwrap(); + assert_eq!(peer.payload(), payload); + } + } +} + +#[test] +fn signed_record_checks_every_trusted_binding_field() { + let expected = binding(); + let (encoded, public_key) = signed(&expected, &[5; 16], &[0, 255, 16, 32]); + let mut alternatives = Vec::new(); + let mut changed = expected; + changed.kind = RecordKind::Policy; + alternatives.push(changed); + let mut changed = expected; + changed.vault_id[0] ^= 1; + alternatives.push(changed); + let mut changed = expected; + changed.generation[0] ^= 1; + alternatives.push(changed); + let mut changed = expected; + changed.epoch += 1; + alternatives.push(changed); + let mut changed = expected; + changed.object_id[0] ^= 1; + alternatives.push(changed); + let mut changed = expected; + changed.author_id[0] ^= 1; + alternatives.push(changed); + let mut changed = expected; + changed.membership_hash[0] ^= 1; + alternatives.push(changed); + for changed in alternatives { + assert_eq!( + UnverifiedRecord::parse(&encoded, 4) + .unwrap() + .verify(&changed, &public_key) + .unwrap_err(), + RecordError::ContextMismatch + ); + } + assert_eq!( + UnverifiedRecord::parse(&encoded, 4) + .unwrap() + .verify(&expected, &[0; 32]) + .unwrap_err(), + RecordError::InvalidSignature + ); + assert_eq!( + UnverifiedRecord::parse(&encoded, 4) + .unwrap() + .verify(&expected, &[0; 31]) + .unwrap_err(), + RecordError::InvalidSignature + ); +} + +#[test] +fn signed_record_authenticates_every_byte_and_preserves_input() { + let expected = binding(); + let (encoded, public_key) = signed(&expected, &[5; 16], &[0, 255, 16, 32]); + for index in 0..encoded.len() { + let mut changed = encoded.clone(); + changed[index] ^= 1; + let before = changed.clone(); + assert!( + UnverifiedRecord::parse(&changed, 4) + .and_then(|record| record.verify(&expected, &public_key)) + .is_err(), + "byte {index}" + ); + assert_eq!(changed, before); + } + for length in 0..encoded.len() { + assert!( + UnverifiedRecord::parse(&encoded[..length], 4).is_err(), + "length {length}" + ); + } +} + +#[test] +fn signed_record_rejects_ambiguous_cbor() { + let (encoded, _) = signed(&binding(), &[5; 16], &[0, 255, 16, 32]); + let fixtures: Fixtures = + serde_json::from_str(include_str!("../tests/fixtures/primitives.json")).unwrap(); + assert!(fixtures.record_mutations.len() >= 21); + for mutation in fixtures.record_mutations { + let mut changed = encoded.clone(); + changed.splice( + mutation.offset..mutation.offset + mutation.remove, + hex(&mutation.insert), + ); + let error = UnverifiedRecord::parse(&changed, 4).unwrap_err(); + assert_eq!(format!("{error:?}"), mutation.error, "{}", mutation.name); + } + let mut trailing = encoded; + trailing.push(0); + assert_eq!( + UnverifiedRecord::parse(&trailing, 4).unwrap_err(), + RecordError::Malformed + ); +} + +#[test] +fn signed_record_bounds_payload_and_limit_arithmetic() { + let context = binding(); + let (encoded, _) = signed(&context, &[5; 16], &[0, 255, 16, 32]); + assert_eq!( + UnverifiedRecord::parse(&encoded, 3).unwrap_err(), + RecordError::SizeLimitExceeded + ); + assert_eq!( + UnverifiedRecord::parse(&vec![0; 257], 0).unwrap_err(), + RecordError::SizeLimitExceeded + ); + assert_eq!( + UnverifiedRecord::parse(&encoded, usize::MAX).unwrap_err(), + RecordError::SizeLimitExceeded + ); + assert_eq!( + signing_bytes(&context, &[5; 16], &[0; 4], 3).unwrap_err(), + RecordError::SizeLimitExceeded + ); + assert_eq!( + encode_signed(&context, &[5; 16], &[0; 4], &[0; 64], 3).unwrap_err(), + RecordError::SizeLimitExceeded + ); + let mut zero_epoch = context; + zero_epoch.epoch = 0; + assert_eq!( + signing_bytes(&zero_epoch, &[5; 16], &[], 0).unwrap_err(), + RecordError::InvalidEpoch + ); +} + +#[test] +fn signed_record_round_trips_integer_and_length_boundaries() { + for epoch in [ + 1, + 23, + 24, + 255, + 256, + 65535, + 65536, + u32::MAX as u64, + 1 << 32, + u64::MAX, + ] { + let context = RecordBinding { epoch, ..binding() }; + for length in [0, 23, 24, 255, 256, 65535, 65536] { + let payload = vec![42; length]; + let (encoded, key) = signed(&context, &[5; 16], &payload); + let record = UnverifiedRecord::parse(&encoded, length) + .unwrap() + .verify(&context, &key) + .unwrap(); + assert_eq!(record.payload(), payload); + assert_eq!(record.binding().epoch, epoch); + } + } +} diff --git a/crates/crypto/src/recovery.rs b/crates/crypto/src/recovery.rs new file mode 100644 index 000000000..989ddec4b --- /dev/null +++ b/crates/crypto/src/recovery.rs @@ -0,0 +1,261 @@ +//! Recovery kit (RFC 0001 §4.1, §6.3; plan Q4-A): a random 256-bit secret +//! shown once as grouped, checksummed Base32, from which separate signing and +//! encryption keys are derived with labeled HKDF. Ordinary devices never +//! retain the secret; they hold only the derived PUBLIC keys (in the +//! membership policy) so they can re-encrypt recovery envelopes after +//! rotation. + +use crate::content::DeviceSigner; +use crate::hpke::HpkePrivateKey; +use crate::{CryptoError, hkdf_sha256, sha256}; +use std::fmt; +use zeroize::Zeroizing; + +const KIT_DOMAIN: &[u8] = b"zeron/recovery-kit/v1\0"; +const SIGNING_LABEL: &[u8] = b"zeron/recovery/sign/v1"; +const ENCRYPTION_LABEL: &[u8] = b"zeron/recovery/hpke/v1"; +const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +const GROUP: usize = 5; +/// 32 secret bytes + 2 checksum bytes = 272 bits = 55 Base32 symbols. +const KIT_SYMBOLS: usize = 55; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RecoveryError { + Crypto(CryptoError), + InvalidCharacter, + InvalidLength, + ChecksumMismatch, +} + +impl fmt::Display for RecoveryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for RecoveryError {} +impl From for RecoveryError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} + +pub struct RecoverySecret(Zeroizing<[u8; 32]>); + +impl RecoverySecret { + pub fn generate() -> Result { + let mut bytes = Zeroizing::new([0; 32]); + crate::fill_random(bytes.as_mut())?; + Ok(Self(bytes)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes: [u8; 32] = bytes.try_into().map_err(|_| RecoveryError::InvalidLength)?; + Ok(Self(Zeroizing::new(bytes))) + } + + pub fn expose_secret(&self) -> &[u8; 32] { + &self.0 + } + + /// The user-facing kit text: 11 dash-separated groups of 5 symbols. + pub fn to_kit(&self) -> Zeroizing { + let mut payload = [0u8; 34]; + payload[..32].copy_from_slice(self.0.as_ref()); + payload[32..].copy_from_slice(&self.checksum()); + let symbols = base32_encode(&payload); + let mut text = String::with_capacity(KIT_SYMBOLS + KIT_SYMBOLS / GROUP); + for (index, symbol) in symbols.iter().enumerate() { + if index > 0 && index % GROUP == 0 { + text.push('-'); + } + text.push(*symbol as char); + } + Zeroizing::new(text) + } + + /// Parse kit text. Whitespace and dashes are ignored, case is not + /// significant, and the checksum catches transcription errors. + pub fn from_kit(text: &str) -> Result { + let mut symbols = Zeroizing::new(Vec::with_capacity(KIT_SYMBOLS)); + for character in text.chars() { + if character.is_whitespace() || character == '-' { + continue; + } + let upper = character.to_ascii_uppercase(); + if !upper.is_ascii() { + return Err(RecoveryError::InvalidCharacter); + } + symbols.push(upper as u8); + } + if symbols.len() != KIT_SYMBOLS { + return Err(RecoveryError::InvalidLength); + } + let payload = base32_decode(&symbols)?; + let secret = Self::from_bytes(&payload[..32])?; + if payload[32..34] != secret.checksum() { + return Err(RecoveryError::ChecksumMismatch); + } + Ok(secret) + } + + fn checksum(&self) -> [u8; 2] { + let digest = sha256(&[KIT_DOMAIN, self.0.as_ref()]); + [digest[0], digest[1]] + } + + fn signing_seed(&self) -> Result, RecoveryError> { + let derived = hkdf_sha256(self.0.as_ref(), &[], SIGNING_LABEL, 32)?; + let mut seed = Zeroizing::new([0; 32]); + seed.copy_from_slice(derived.as_bytes()); + Ok(seed) + } + + /// The recovery signing identity; its author ID is derived from its + /// public key (`policy::recovery_authority_id`). + pub fn signer(&self) -> Result { + let seed = self.signing_seed()?; + let probe = DeviceSigner::from_seed([0; 16], seed.as_ref()) + .map_err(|_| RecoveryError::Crypto(CryptoError::InvalidKeyLength))?; + let public: [u8; 32] = probe + .public_key() + .try_into() + .map_err(|_| RecoveryError::Crypto(CryptoError::InvalidKeyLength))?; + DeviceSigner::from_seed(crate::policy::recovery_authority_id(&public), seed.as_ref()) + .map_err(|_| RecoveryError::Crypto(CryptoError::InvalidKeyLength)) + } + + pub fn signing_public_key(&self) -> Result<[u8; 32], RecoveryError> { + self.signer()? + .public_key() + .try_into() + .map_err(|_| RecoveryError::Crypto(CryptoError::InvalidKeyLength)) + } + + pub fn encryption_key(&self) -> Result { + let derived = hkdf_sha256(self.0.as_ref(), &[], ENCRYPTION_LABEL, 32)?; + Ok(HpkePrivateKey::from_bytes(derived.as_bytes())?) + } +} + +impl fmt::Debug for RecoverySecret { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("RecoverySecret([REDACTED])") + } +} + +fn base32_encode(bytes: &[u8]) -> Vec { + let mut out = Vec::with_capacity(bytes.len().div_ceil(5) * 8); + let mut buffer = 0u32; + let mut bits = 0; + for byte in bytes { + buffer = (buffer << 8) | u32::from(*byte); + bits += 8; + while bits >= 5 { + bits -= 5; + out.push(ALPHABET[((buffer >> bits) & 31) as usize]); + } + } + if bits > 0 { + out.push(ALPHABET[((buffer << (5 - bits)) & 31) as usize]); + } + out +} + +fn base32_decode(symbols: &[u8]) -> Result>, RecoveryError> { + let mut out = Zeroizing::new(Vec::with_capacity(symbols.len() * 5 / 8)); + let mut buffer = 0u32; + let mut bits = 0; + for symbol in symbols { + let value = ALPHABET + .iter() + .position(|candidate| candidate == symbol) + .ok_or(RecoveryError::InvalidCharacter)? as u32; + buffer = (buffer << 5) | value; + bits += 5; + if bits >= 8 { + bits -= 8; + out.push(((buffer >> bits) & 0xff) as u8); + } + } + // Leftover bits are padding and must be zero for a canonical kit. + if bits > 0 && (buffer & ((1 << bits) - 1)) != 0 { + return Err(RecoveryError::InvalidCharacter); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kit_round_trips_with_grouping_and_tolerant_parsing() { + let secret = RecoverySecret::generate().unwrap(); + let kit = secret.to_kit(); + assert_eq!(kit.len(), KIT_SYMBOLS + 10); + assert_eq!(kit.split('-').count(), 11); + assert!(kit.split('-').all(|group| group.len() == 5)); + let parsed = RecoverySecret::from_kit(&kit).unwrap(); + assert_eq!(parsed.expose_secret(), secret.expose_secret()); + let sloppy = kit.to_lowercase().replace('-', " "); + assert_eq!( + RecoverySecret::from_kit(&sloppy).unwrap().expose_secret(), + secret.expose_secret() + ); + assert_eq!(format!("{secret:?}"), "RecoverySecret([REDACTED])"); + } + + #[test] + fn kit_detects_transcription_errors() { + let secret = RecoverySecret::from_bytes(&[7; 32]).unwrap(); + let kit = secret.to_kit(); + let mut damaged: Vec = kit.chars().collect(); + damaged[0] = if damaged[0] == 'A' { 'B' } else { 'A' }; + let damaged: String = damaged.into_iter().collect(); + assert!(matches!( + RecoverySecret::from_kit(&damaged), + Err(RecoveryError::ChecksumMismatch) + )); + assert!(matches!( + RecoverySecret::from_kit(&kit[..kit.len() - 1]), + Err(RecoveryError::InvalidLength) + )); + assert!(matches!( + RecoverySecret::from_kit(&kit.replace('A', "1")), + Err(RecoveryError::InvalidCharacter) + )); + // Fixed vector pins the alphabet/checksum across languages. + assert_eq!( + &*kit, + "A4DQO-BYHA4-DQOBY-HA4DQ-OBYHA-4DQOB-YHA4D-QOBYH-A4DQO-BYHA4-D2MCI" + ); + } + + #[test] + fn derived_keys_are_stable_and_separate() { + let secret = RecoverySecret::from_bytes(&[9; 32]).unwrap(); + let again = RecoverySecret::from_bytes(&[9; 32]).unwrap(); + assert_eq!(secret.signing_public_key(), again.signing_public_key()); + assert_eq!( + secret.encryption_key().unwrap().public_key(), + again.encryption_key().unwrap().public_key() + ); + assert_ne!( + secret.signing_public_key().unwrap().to_vec(), + secret + .encryption_key() + .unwrap() + .public_key() + .as_bytes() + .to_vec() + ); + let signer = secret.signer().unwrap(); + assert_eq!( + *signer.author_id(), + crate::policy::recovery_authority_id(&secret.signing_public_key().unwrap()) + ); + let other = RecoverySecret::from_bytes(&[10; 32]).unwrap(); + assert_ne!(secret.signing_public_key(), other.signing_public_key()); + } +} diff --git a/crates/crypto/src/tests.rs b/crates/crypto/src/tests.rs new file mode 100644 index 000000000..1b5bacd3b --- /dev/null +++ b/crates/crypto/src/tests.rs @@ -0,0 +1,344 @@ +use super::*; +use ring::{aead, signature}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct Vectors { + version: u32, + aes256gcm: Vec, + ed25519: Vec, + hkdf_sha256: Vec, + ed25519_point_encodings: Vec, + ed25519_scalar_encodings: Vec, + ed25519_rejections: Vec, +} + +#[derive(Deserialize)] +struct EncodingVector { + name: String, + encoding: String, + allowed: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RejectionVector { + name: String, + public_key: String, + message: String, + signature: String, +} + +#[derive(Deserialize)] +struct AesVector { + name: String, + key: String, + nonce: String, + aad: String, + plaintext: String, + ciphertext: String, + tag: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SignatureVector { + name: String, + seed: String, + public_key: String, + message: String, + signature: String, + peer_signature: Option, +} + +#[derive(Deserialize)] +struct HkdfVector { + name: String, + ikm: String, + salt: String, + info: String, + output: String, +} + +fn vectors() -> Vectors { + let fixture = std::env::var("ZERON_CRYPTO_TEST_VECTORS") + .ok() + .map(|path| std::fs::read_to_string(path).unwrap()); + let vectors: Vectors = serde_json::from_str( + fixture + .as_deref() + .unwrap_or(include_str!("../tests/fixtures/primitives.json")), + ) + .unwrap(); + assert_eq!(vectors.version, 1); + assert!( + !vectors.aes256gcm.is_empty() + && !vectors.ed25519.is_empty() + && !vectors.hkdf_sha256.is_empty() + ); + if fixture.is_some() { + assert!(vectors.ed25519.iter().all(|v| v.peer_signature.is_some())); + } + vectors +} + +pub(super) fn hex(value: &str) -> Vec { + assert!(value.len().is_multiple_of(2)); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect() +} + +#[test] +fn aes_known_answers_match_published_and_swift_vectors() { + for v in vectors().aes256gcm { + let key = hex(&v.key); + let nonce = hex(&v.nonce); + let aad = hex(&v.aad); + let plaintext = hex(&v.plaintext); + let ciphertext = hex(&(v.ciphertext + &v.tag)); + let opened = open_aes256_gcm(&key, &nonce, &aad, &ciphertext, plaintext.len()).unwrap(); + assert_eq!(opened.as_bytes(), plaintext, "{}", v.name); + let mut sealed = plaintext; + let key = aead::LessSafeKey::new(aead::UnboundKey::new(&aead::AES_256_GCM, &key).unwrap()); + key.seal_in_place_append_tag( + aead::Nonce::try_assume_unique_for_key(&nonce).unwrap(), + aead::Aad::from(&aad), + &mut sealed, + ) + .unwrap(); + assert_eq!(sealed, ciphertext, "{}", v.name); + } +} + +#[test] +fn aes_rejects_tampering_without_modifying_the_input() { + let v = vectors().aes256gcm.remove(1); + let key = hex(&v.key); + let nonce = hex(&v.nonce); + let ciphertext = hex(&(v.ciphertext + &v.tag)); + for index in 0..ciphertext.len() { + let mut damaged = ciphertext.clone(); + damaged[index] ^= 1; + let before = damaged.clone(); + assert_eq!( + open_aes256_gcm(&key, &nonce, &[], &damaged, 16).unwrap_err(), + CryptoError::AuthenticationFailed + ); + assert_eq!(damaged, before); + } + assert_eq!( + open_aes256_gcm(&key, &nonce, &[1], &ciphertext, 16).unwrap_err(), + CryptoError::AuthenticationFailed + ); + assert_eq!( + open_aes256_gcm(&[1; 32], &nonce, &[], &ciphertext, 16).unwrap_err(), + CryptoError::AuthenticationFailed + ); + assert_eq!( + open_aes256_gcm(&key, &[1; 12], &[], &ciphertext, 16).unwrap_err(), + CryptoError::AuthenticationFailed + ); +} + +#[test] +fn aes_validates_lengths_and_budget_before_opening() { + let v = vectors().aes256gcm.remove(1); + let key = hex(&v.key); + let nonce = hex(&v.nonce); + let ciphertext = hex(&(v.ciphertext + &v.tag)); + for length in [0, 16, 24, 31, 33] { + assert_eq!( + open_aes256_gcm(&vec![0; length], &nonce, &[], &ciphertext, 16).unwrap_err(), + CryptoError::InvalidKeyLength + ); + } + for length in [0, 8, 11, 13, 16] { + assert_eq!( + open_aes256_gcm(&key, &vec![0; length], &[], &ciphertext, 16).unwrap_err(), + CryptoError::InvalidNonceLength + ); + } + for length in 0..16 { + assert_eq!( + open_aes256_gcm(&key, &nonce, &[], &ciphertext[..length], 16).unwrap_err(), + CryptoError::InvalidCiphertextLength + ); + } + assert_eq!( + open_aes256_gcm(&key, &nonce, &[], &ciphertext, 15).unwrap_err(), + CryptoError::SizeLimitExceeded + ); +} + +#[test] +fn ed25519_known_answers_match_published_and_swift_vectors() { + use signature::KeyPair; + for v in vectors().ed25519 { + let key = hex(&v.public_key); + let message = hex(&v.message); + let sig = hex(&v.signature); + verify_ed25519(&key, &message, &sig).unwrap(); + if let Some(peer_signature) = &v.peer_signature { + verify_ed25519(&key, &message, &hex(peer_signature)).unwrap(); + } + let signer = signature::Ed25519KeyPair::from_seed_unchecked(&hex(&v.seed)).unwrap(); + assert_eq!(signer.public_key().as_ref(), key, "{}", v.name); + assert_eq!(signer.sign(&message).as_ref(), sig, "{}", v.name); + let mut changed = message; + changed.push(0); + assert_eq!( + verify_ed25519(&key, &changed, &sig), + Err(CryptoError::AuthenticationFailed) + ); + for index in 0..sig.len() { + let mut damaged = sig.clone(); + damaged[index] ^= 1; + assert_eq!( + verify_ed25519(&key, &hex(&v.message), &damaged), + Err(CryptoError::AuthenticationFailed) + ); + } + } +} + +#[test] +fn ed25519_rejects_invalid_lengths_and_wrong_keys() { + let v = vectors().ed25519.remove(0); + let key = hex(&v.public_key); + let sig = hex(&v.signature); + for length in [0, 31, 33] { + assert_eq!( + verify_ed25519(&vec![0; length], &[], &sig), + Err(CryptoError::InvalidKeyLength) + ); + } + for length in [0, 63, 65] { + assert_eq!( + verify_ed25519(&key, &[], &vec![0; length]), + Err(CryptoError::InvalidSignatureLength) + ); + } + assert_eq!( + verify_ed25519(&[0; 32], &[], &sig), + Err(CryptoError::AuthenticationFailed) + ); +} + +#[test] +fn ed25519_encoding_prechecks_match_shared_vectors() { + let fixtures = vectors(); + assert!(fixtures.ed25519_point_encodings.len() >= 13); + assert!(fixtures.ed25519_scalar_encodings.len() >= 9); + for v in fixtures.ed25519_point_encodings { + let mut encoded = hex(&v.encoding); + let before = encoded.clone(); + assert_eq!( + ed25519_point_encoding_precheck(&encoded), + v.allowed, + "{}", + v.name + ); + assert_eq!(encoded, before); + if encoded.len() == 32 { + encoded[31] ^= 0x80; + assert_eq!( + ed25519_point_encoding_precheck(&encoded), + v.allowed, + "{} opposite sign", + v.name + ); + } + } + for v in fixtures.ed25519_scalar_encodings { + let encoded = hex(&v.encoding); + assert_eq!( + ed25519_scalar_encoding_precheck(&encoded), + v.allowed, + "{}", + v.name + ); + } + for low_byte in 0xed..=0xff { + for high_byte in [0x7f, 0xff] { + let mut noncanonical = [0xff; 32]; + noncanonical[0] = low_byte; + noncanonical[31] = high_byte; + assert!(!ed25519_point_encoding_precheck(&noncanonical)); + } + } +} + +#[test] +fn ed25519_rejection_vectors_fail_closed() { + let fixtures = vectors(); + assert!(fixtures.ed25519_rejections.len() >= 8); + for v in fixtures.ed25519_rejections { + let key = hex(&v.public_key); + let message = hex(&v.message); + let sig = hex(&v.signature); + assert_eq!(key.len(), 32); + assert_eq!(sig.len(), 64); + assert_eq!( + verify_ed25519(&key, &message, &sig), + Err(CryptoError::AuthenticationFailed), + "{}", + v.name + ); + } +} + +#[test] +fn ed25519_rejects_identity_key_signature() { + let mut identity = [0; 32]; + identity[0] = 1; + let mut signature = [0; 64]; + signature[0] = 1; + assert_eq!( + verify_ed25519(&identity, b"synthetic key-admission probe", &signature), + Err(CryptoError::AuthenticationFailed) + ); +} + +#[test] +fn hkdf_matches_rfc5869_and_separates_labels() { + for v in vectors().hkdf_sha256 { + let ikm = hex(&v.ikm); + let salt = hex(&v.salt); + let info = hex(&v.info); + let output = hex(&v.output); + let derived = hkdf_sha256(&ikm, &salt, &info, output.len()).unwrap(); + assert_eq!(derived.as_bytes(), output, "{}", v.name); + let mut changed_info = info; + changed_info.push(0); + assert_ne!( + hkdf_sha256(&ikm, &salt, &changed_info, output.len()) + .unwrap() + .as_bytes(), + output + ); + assert_eq!(format!("{derived:?}"), "SecretBytes([REDACTED])"); + } +} + +#[test] +fn hkdf_bounds_output_before_allocation() { + for length in [0, 8161, usize::MAX] { + assert_eq!( + hkdf_sha256(&[1; 32], &[], &[], length).unwrap_err(), + CryptoError::InvalidOutputLength + ); + } + for length in [1, 32, 8160] { + assert_eq!( + hkdf_sha256(&[1; 32], &[], &[], length) + .unwrap() + .as_bytes() + .len(), + length + ); + } +} diff --git a/crates/crypto/tests/channel_fixture.rs b/crates/crypto/tests/channel_fixture.rs new file mode 100644 index 000000000..7fa3c7c7a --- /dev/null +++ b/crates/crypto/tests/channel_fixture.rs @@ -0,0 +1,83 @@ +//! Reproducible Snow transcript consumed by the Swift channel conformance tests. +use serde_json::json; + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +#[test] +fn swift_channel_fixture() { + let prologue = [b"zeron/device-channel/v1\0".as_slice(), &[1; 16], &[2; 16]].concat(); + let build = || snow::Builder::new("Noise_XX_25519_AESGCM_SHA256".parse().unwrap()); + let mut initiator = build() + .local_private_key(&[3; 32]) + .unwrap() + .fixed_ephemeral_key_for_testing_only(&[5; 32]) + .prologue(&prologue) + .unwrap() + .build_initiator() + .unwrap(); + let mut responder = build() + .local_private_key(&[4; 32]) + .unwrap() + .fixed_ephemeral_key_for_testing_only(&[6; 32]) + .prologue(&prologue) + .unwrap() + .build_responder() + .unwrap(); + let mut buffer = vec![0; 65535]; + let mut plaintext = vec![0; 65535]; + let n = initiator.write_message(&[], &mut buffer).unwrap(); + let first = buffer[..n].to_vec(); + responder.read_message(&first, &mut plaintext).unwrap(); + let n = responder.write_message(&[8; 16], &mut buffer).unwrap(); + let second = buffer[..n].to_vec(); + initiator.read_message(&second, &mut plaintext).unwrap(); + let peer_key = initiator.get_remote_static().unwrap().to_vec(); + let n = initiator.write_message(&[7; 16], &mut buffer).unwrap(); + let third = buffer[..n].to_vec(); + responder.read_message(&third, &mut plaintext).unwrap(); + let mut initiator = initiator.into_transport_mode().unwrap(); + let mut responder = responder.into_transport_mode().unwrap(); + let messages = [ + Vec::new(), + b"hello from the phone".to_vec(), + (0..200_000).map(|i| (i % 251) as u8).collect(), + ]; + let mut seal = |message: &[u8], from_phone: bool| { + let (writer, reader) = if from_phone { + (&mut initiator, &mut responder) + } else { + (&mut responder, &mut initiator) + }; + let chunks: Vec<_> = if message.is_empty() { + vec![&[][..]] + } else { + message.chunks(65518).collect() + }; + let mut sealed = Vec::new(); + for (i, chunk) in chunks.iter().enumerate() { + let body = [&[u8::from(i + 1 < chunks.len())][..], chunk].concat(); + let n = writer.write_message(&body, &mut buffer).unwrap(); + sealed.extend_from_slice(&(n as u16).to_be_bytes()); + sealed.extend_from_slice(&buffer[..n]); + let count = reader.read_message(&buffer[..n], &mut plaintext).unwrap(); + assert_eq!(&plaintext[..count], &body); + } + hex(&sealed) + }; + let frames: Vec<_> = messages + .iter() + .map( + |m| json!({"plaintext": hex(m), "outgoing": seal(m, true), "incoming": seal(m, false)}), + ) + .collect(); + let fixture = json!({"first": hex(&first), "second": hex(&second), "third": hex(&third), "peerKey": hex(&peer_key), "frames": frames}); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/channel.json"); + if std::env::var_os("UPDATE_CHANNEL_FIXTURE").is_some() { + std::fs::write(&path, serde_json::to_string(&fixture).unwrap() + "\n").unwrap(); + } + let existing: serde_json::Value = + serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap(); + assert_eq!(existing, fixture); +} diff --git a/crates/crypto/tests/fixtures/channel.json b/crates/crypto/tests/fixtures/channel.json new file mode 100644 index 000000000..3e2ea7858 --- /dev/null +++ b/crates/crypto/tests/fixtures/channel.json @@ -0,0 +1 @@ +{"first":"50a61409b1ddd0325e9b16b700e719e9772c07000b1bd7786e907c653d20495d","frames":[{"incoming":"0011897613e7ddb9f58b69920881eaac098122","outgoing":"0011438437135a1d3d6652366de594d0dedf95","plaintext":""},{"incoming":"002502ed93b5830865673f9974209ee71f0340edb8c88f9d7e4e0604af6becf4dc4c5d77f5136e","outgoing":"00252f28d6922107fdf0768f45af106accd82b150fb7ea4118b9beb2d53bb13ae8abb0d9a42478","plaintext":"68656c6c6f2066726f6d207468652070686f6e65"},{"incoming":"ffffa37a705842ddf929ab425f290ed3e6d0316e3d382c0b84a8116cf728c75ea832565cbcf21c9fbe3e9cdf5264d3016ec54147ab1142397bee3ad6f2628929633d9240e3e8ece246af5d37cdd7207388463b5ec6209ce0fa5f7792ba5c66be68bdf0b331ac2ea9339e8fbd1f893a05de66ff1d603c72a7ec3cfa0b6fd3fe828cceb85428a1203311378ac8e57ba6e5137be02814b49ade783545aed59296387cb43de932fe09e3657dd1c8255bb5391949f16f0cf9d87a69ea6008cb0ac07a1c6af690c3dddea3d8de15304db9855b718daa9651be6d2127b4dda5e372fff127d387f73ca6377cc7dae0ae6a6518c9dc9a271cee2920a855dc6617925f635cf1ee7579b135c1f356ef835c1e9817f2752d7677005097816dee8529aa2f57f9fd34f650e94248ff2baf7e604ccbdccc11f3e0eaca8309649bdd6545794597f805224b4dac6acd01840de1cfc16bc1435eda9c3c3d272add8d89c00685e8d9572f11b951fe97fa999909d327c3ab93d727cab85b1f0d9797abca07ada1634044809ac449679096994d269b1feb9c47b3a628e81fb497af71858f12747345dce9619432b0910406dc7516af1f742f434ae1b9fd30a8214c65491322434a3c2c1247114fee6fcbeca0bae4d5aa08f614887c6e6a3361e160b62e81b532185bdaedc857ba95a5d3d2cd9747eb4b81d0f6f522c0a9d40189a6f4094d68f6a624b660648d7654e335bb4df9a243bdd5f7cdb09d6d0ecddcbccbcd72f0a232ff248ff82e638a10851182b2c1a907a27422a1bc25863c058757098e304b12da7180d3e46e78957658c98a26546bc6d4556387ad93df5034725735156b32b698b45b1f460336e26d3c8eabe6289d0b8c6409e43b4af5f3ea841d7506e8130e5a9b51634a042feccfa5fb02284585823ad7886af70257a5e5058afbcba97b25448df74a074cc85160152762c1f0dc4b10cfed00ec5d829361df85a3b1bac3d9a59cffc320a27ab7cfb4d1d62dff73870ced56fd52041d02bb5c1de8d09b7e2badce352d13c35d757489270eccb1c6c774927b301f6aeef78c7990923938ebd7d420fdbff5a2e12476041a66a8f0cbb8ddfbe4e435cba9d65f6a6c846adb3d003bf51514faac87c04b465cd1e669eea7cfc8726eddbe3549cb93b7176babb84a9237bc9267bd408c1447eded54579a47d3741892cd12f01040db4233946050c7b4cfea0886c1e3dae9ab116c1b6633581ddb4b094f3401f9199bcb30c504f030e9ed9a9ab0e2a98a6ea455600ca57a5577a2363de893d503ffb1b70593ec4c5491bcfa2666b0edda82a2e4972d2bbe1dbd36ddea04f8e54296d5c0ea063e68be23ab6da1d16fd51f4aec4801ebd26a1f0e4c5d0b558a06023fb277748aa72f0d89ebdd37422bfbddf0fb41245aeee2aaec88bbefa775d93b161ee8ec0948910a949a9539696bf33e3676801bffe372db571830c0377446afd70f827bee24efad40f500ae1de4850ba1fa9059649a89cff97dcc876ab6e18cffc6f81975029e0763346d63bbb76251346e81be198c027ac5ca54e94738231c6632f47bb87c50f6c303b292b8992b3081e7667f268f0df3dc9ad15dbc859d045ba054c409b647e5b0f0d0b923363f73c68909f6c4f6e25d2e165070beb80eb5f3d1707fa9d34801f2de738e14cc56005b1a5dbdb04e7f984790d397c6d4ebe5f773c55de8f8ecc7ad554ad52de58d81fe7e405f5760306fd1d39d4b6b946608ad4b260098b4b33322c9aa2b10efe4cdbf038106cd69ed607396b57be6a023c28adbfab74b83d45ac3977b594aefde4367d79d834c3e91b8cb8d9282988de5fbd1a7efe2c6a7fcd80d8e48add9eebc662e91ccb7739f4f82e2f53c6502e133e5d17ccc6d2be9a051d60abe83381b192d9850863886a9c87d4198fe8e6e24b8216f9de6c364b2ae51b92e4fbd5b39951fb8422d5a586ceaa733ffe35d4da3dfdae8798553913e3bdebfd4643d6d5592b02975240bf8cbac4838d4582df74e364f7709dfd36d62156f8f82da3ea669cfad49bd1fbcd458870a30113941231b3b5368c884719338d1fe4c0eb31e5d6d95e049649fc8299ecdd5c23c8998fa90d7cb93c88b75050a7ac5161844f54c53fa0817324e31345c88d4ece63fb8129516d6c74007128beeaad70d34d3e9d63c26998f57d38601ae61519ac3a91acb0f829d8675c009af6ede67fbde1dc046995a6595d3a859138c3a6b3204e0cb3ce7729c315e1d5c6eb6bd0af10eea07bb7764b0e197ecfb596c61f255ee71eb132eb4b4c47074f414b18432882d246d41106708febbac048c57e9c88d946a7132f55e30ba2f3af8f4139e209fb9664abebe8b0682bb048e566ba58030d545ef8d1d1bf90d4255ce6cb645bc92d9aaac21863ae1a8f62282dcb5a4fe74b21c131117591942cdf1f543fe71a010fac0fec072ae5cc0d3c07c9516ea17da97104dbb8f27f7992df2b067e333313547611bae1b0da61fc37e391783f587a215db6dd656b2391b9cf64d7c0177bf87dd7e86da8ea5267f45d0d8b09dcdf8008614f118221c7815a0b51f02818eb2407b6367aa8cc333d18003e61e0c9b4bd186a7694016270b06dc9e7b13a377e14362f7b1dea2544ed89625ad2065fc940e11704f68752a4fe6a8347f9a51fb21c579853ba79e2c4257a952a425617b662c04645659a7f124ba72d9a6964807ee07cbed79004348abfae900d6567077403098a204923c32176513df7d5ba35acd2ec1ef276953cdd0163cdb52207e838ec2ea5f2521c3f072d6497bb76141fd318bcad1d0347e7b4cb051a6fefef4cb72f2338a2e08d3818764477fc8bfe002e3d145c46f457289be4629e73f34abf6e42fcba13472691126210578ff444c8a6892d07a84e88c519d12e25234d5c164aba6f33e7e413c2a7742b72391a38e10bd12a31de81abb1c9ceb78b015cebea1646c6dca965cbf2649396ad708ab003a4738b97bff1d96e2c3cd8386a46cfef7e1000f86732bb9b6369520d31376d84e101c278f0bcf5dd64ea75d811f56dacd776931711f7231dace325f0089c340e5f2fe8e8878e11efc563e35c313b5f8912a99c0f8592cf4e969cd9d6b55c6877a40bed04ba91ec9c0ec604d13412014751c208d38416d42298ceccbb94ce618a1d66c57993f3383008a107d376906b5abb8900208f729e03ef794ebda528734bbb64bf171b629fdf9eb9bcacc0ee4e22c37827f2b4e6f95f155dac6588eb8bd8c1b2468a860fad7f1992deeb00d3705904e54f873656bf3166ccfd23e25d5940e9f7ae43e98a9a537d5c22762c0bb17d25bc81c9ed51e2fb21bca580d279b46bbaa42b6bdba3daa3e0cde76f2d2816bb6bbda4a076dcd07a3b13d017c16654b2ad1f3164e15605d4b5f677c3669ec182147caa45cfbfab52a68b2cc8e441e82277939d865a08c7902b78e4f7b4946f324ee590f75b9e27dc715a950d41870c62b7158458cc8eef7b6bef25429208d54a0a227742c35699ee2400379341babeac048c9cde9dec6a337bf0504d14bfb67961ba58e0231c131468eff2c84b95dd3a11f11697fb1da3804683a97023a44261f8c024b4c4a120eb552c637af7ae264d312f9c4d3381a7a555f2170b3a3846e4464d57781c7c85a1aab5ed5b074cd3881a3060125a5706dc106c8ee378b98f87edf7606dbf4511ae4f02ca4635fcd604f31b9939ffdb4f025c70b43fa939ee2280ee77a5657eb22886fa3ddfb60c86fd351c7f002976e7b414cd1c85337a838ab545ab53a131b7534c0ff3bf6912813a03fceefec9063f945a208b6a0a2fdf4c393bcc38567c2ee625ab644ae299818cb0ea88e18221c6017eddca71809bf1f010ffe5ad27dffaf13b7ef457c9f9b7945b4f95719c075299dd01061a2b638010659f446ad32e19e3ef77ecb17ff5d0b8b94d65f1179cc4e18887929e45d0427a256125feaf26fb589f886cd9bb5b0fb773f338a9fb5821b084470f83a99424592aa7c6e6e1e5180919aeabfd9fe5145aeb5fd55afe35414779abc189f637b38fe70a89d8ccc40bdf879a2dd410ade8e1fc24bf6074bd3b4c96ee891216e5e8dd18c2ae527a22032a801d477d02f202fb76834bea838af2bfb7e7d06c4fca4045e2e2d8c9e67b5823f02d31f0a39516f110c4d895b306d1c57383e70cf01661dc7e3618efe66fcc38ad21ad3759f69dbc3339c5bcddb1cbb5b4890d65bdf5addde173874ec602f113ab9f829cfe70060a5e42ab8eb4fedb1c06d349c8487c75fc73c8fbb1cee65abe0fecf89183058e771e36dc8b83ee20bedaa57608ca0dc8b95f242af791551e3c38195331f057e390ff965c023d0604dd5e1855f9608e73b2318364ca1f562bff347419caacbc5103d7240d8a4a2f0feb07adeb47f271dc9e8eedea49502cf7bc771d32f49f262d8596f38cf996363cff84a050567a47d8327be8c7132be6f85613276ca02780cadbf6c3abaf1e7bf2756bdd0d3b6d2c233d2d7132004559b57e863457d8e3eea96a5bb8f8d59f204ffb47e5367aec9c13a76b7f3b135f07c08b73bd2adc61e5a7e43113be39865253c1b37f5f58c2a4353e31b198bf07bf04b090a122873f14df102cd7b25bab084730ffdd6e970fb46bdd1a04f9ae3a2cca8c12e057a44d415dd4a5b364f93e436ffd01dfc5f3d25ddd89eab1c25ef63a5163c79dd350d43f9813734baefcdcc67886d9a6feada4477f2812b4d729d489cea8e30415121c4c6c3db1ea3e9aa6556cdd3a1184cd1d890f8d9ccfd9241d9173b4f7c319271d5c95f73226a9b1445632fb6ccc67a45d47917d71c6ce1c49f15533799e8bda1dab5787f2d1fc3097d466d476878e9c84bc114d78f9280b1dff9c75244b41cd01a9766dfe0e536915ac8b289f9aa8d61ee2fd12af60cfe2468cd29faef8d990bb88c171594f85844a65b82fc9ee060f517ffa3503ac4c1f34b5c442292545ab135e8a5d2c04f8d733d04f297a8e37a2b3f96d02b639493b40f4c8d62205e0acc2929a9dc9755f8e0ec6617a6fa304b57875c271232c5bc7f86ba73cad1d9ac3e7f430b19f0c0d4f0fd811a6293b5ede9db4b406f546ed0493c5787fcab916cb7b42ff3a79c9d71867d4f30f582b8cde73a69c7ba4f7db312ab50d59132f1f14cff158ad3e72f481c22fd6569eb8305e016a0dd45bf3d9ca8e9676117805fde1bf62323236b9d3d2fdd2dab49a1bbf7b5339328bd361a5b5967d77775292c2aa40bae5270981951ad0ff873a802d24e01ab1f6c49600c759a4b66939ef9b3e24e7c8236d65ba349899d4e581148555f7ed85af5eae205856dd8e7cbbb374f32f51a0dc68f4cf6ef90ae34e2a62e8d761f356d4c04fe6d8b09dc88487fe3e425cb25cbd0edfe23d18a0f557ac27ba0b7df286338ca948d23baa86418a578ac9cad9f69403dcf6e61359e93bbc3c14ee08d8fbe89b6266ff23b62e316ab7990b107aa3dadadc0b440659aaa34ae231c13a05054f52f1fac8a5cf8ab15681954e8bfdda46a378d764564d53a6eadc4cd048e06fba89a09a67fdb14c2e383eb1bef858640bbcb24c1e8e71a542b76fde12098c8381440eba7807c490503207a33e23ae920f76ee8b8bb395502f335348dcc356061fa09e80ac328adac23683af0341212f9aa40905f2e079cb739ee6fd2eb6ecaf9a53f8e39b474895a6648cdaabcd734bafc6bc3af71896598e5d000a832f2997857dc9e5ddf2934793c87cf2c020af9b42db0ef307c905b8a06367da4eb480578cf87b770f614b558e6c58f05e01af086201fca257806718e032ae81031a5125a903097d87eed954c0acc0ae6c597bd1f4c4727d1ccadd22fff818e7835df26ec43f7c7c20ba48ba554579e58af2738e95360d20067c8ff8c239d821a5d3bfcbe32c61d30909a4cbf9095fb1cbb4f947bf0d8c97f6c9d55f1f2179e6af4671716a5cbfb8db3dc00e4a10e996b787df861a570d71f1a79e0e7d679e47b38c01c8d753f4e8e3534495d0c4ac60af05703a27724224a3dcac354c0acabe1adf6eb6bd22186c601bfd7d6daede4ffbb695ef18ffe7b41335c90595333a00f693a46d22ee1b841ef68a7233f11c2557a859ce17f88f64da2e8cd032e00d723d2f156a9647b6c5366f8abf5b55e3581e86d6f9e6ad5e5a01eb577fa7a05782c233ec06fa9a1f402768cd7209726bc0fcf400ad95c96bc03765ad4adccfa640abf6e4e4ebde1b952c21585c38723edab8e3ff8a2f55665e23dc15c12f4c7fdbfa0544cbe9d0e3d0ae78a6007c64e78eaa46108ab0bc210b9920f7fec5145a6e7ce6741cf5f702164a991452f6c880e92988b09ec8d91fc209449ba10f50f6a62651a0b8220a0ef2905a7f52da5f0d589d70c9ac6882215857bd2924b27bd541d140d081a59f9fc61270ab2e1efb1ce794baee40e398a9da12af640b1ad54a2501ad3e29df2cbe69316ffd7ebb0a13f791280ddffba4e7af9ef1830819de9b9b4238c8aee7eb03c2a395ba90071725f7ff332e01e5dc23c790ed8aba3eddee00b638ec13e684039e2332fd5a2a1cc36461ec05fc9e75e93d0f488823e7856e5632f6c3ad483e6841000632b2e650aadecddd96ccdeaf2d5ba0792f36d70bdf94f46966f139ee9600383614a3916a7846151a5ee31a470c92e70ee2c57b03f0408813b07fec9b2d40e738bc96bc16c76e22553d864cb6f942fa0bd196366559fea6413aa4fb34468c5c25fcec58fa8532f2f7fc18d4c399193281402cdb355da49c0419cd91c999fa4d935c641f1c221d5aa59158b491ee447205a1a34351a34bfad55345b4a7f564822764bb86a6b438b136582d24d296548134817360dad797c4f1b5bcc51f8b91024474205860cb22c781d0ed49a00c138fc3526403b99044cf1aa6a4a755e5e5ccbf7ec2213440b734339c64d9c7d5f0d9eba96f73f27d9cdf9a0bb2b45ab173d27a87fac8d0eaf9d1b708b21639f232dfa275bcf7c992f586f33c95876e06572bec5da09d9fe0445aeaa672512855441b2b7c4d5cbbe62c16c90f2c4fd76a1ddd087aeba25474f572f822d357918c7b90cdb085fba198fbc1a3e193f94828cdbf135b5c7c0f186263eb220753d06bdea66fa648663eef91ef504255aee30d34fc6d349ec99b512e601ad04e6850f4bec4293314a4e9ed4e152bb0422d943db27d5d3fd2b991a489e5a5b064acc85a0991cbd14ccc88da5f67effdc0750161eb92cd9bcd89d1c5f8eae276a50dfeb94967fbb69341a58f5c765075c0013b7fae47e899a88541b7ffc89a204bea8b21f8a899fc3649ad5b41a98a9226244e188cddea8e99e12a2c573080ce2a91b268a4fa56062cb6cc808dc19a2b340216ef19e225330d8c1bd7d7f1e440647f1b2bd1d35a1d743ba2db946f6bcb5578ad020b82c27565c51e999f5e49229badc5defb5d32dc6399c6394239fa90abc36c715d4ed791541d70b2f73cb5deaf496dcd1f6e53935b329c77a143a18f4d3194e9c07f9953270abe0b15c9d7c9ed43167110f88f62ba3e4b85e6513a999ee8481a7e58a44c4791fc9fb59b06a198532efdf8a0ca7701838efe5043e15846f9c37cfcc2569d6c09942e750f13aacce8af8f5b12f51544e61c5ae55e68513318c3de5828b0aeec6bb47a34f2768b0a658edb32269c0565299d5a9f082de6d281bc07d179a1f1914995399707dbb4c68ba284d80aa1d9ba4fff9f98c3327af9b21e4be9d5c8aea5f3716ac4be39df25ec5656ddf274cae46184f90ec119e63b21436026562de7f07674fe5861c74ba6651b95d01c714ee7ff751b048eb06f054d1d41fb8ed606fec1d1d7bd39e4d7b7d8358c16211486ba769aa5eaaeaca95949b191b6767ad84852dc437a54db0d1b3af324359e4a3bddfbb362a8779d6851cc40c49d68847b0dcfc619ccbd45d9a37df29f72d20b4fab47257e5aa85e016eb4389e393b28aca62110911f456ef766841b2d0d8c28338d6a1fce545cf4ce6c27daeb0d10f8acd4425d2ad325ffbca76e3ae5129e1f3cafef183a90e8d402219534d52301b261c6c11ebea3faa78bc27f01151d90d36e7b828177a9c5e934b2a798c49da3b64214a927374cafd2c5c49cc581165de39e2aa415976fcc7d1c1af0171e991081fd92883b654ed9c690372eb5d92541228dd1c17f1e7d4d84dc7d42df8ca5491a2ef69af69607b1ce96e8419d90f0b8e5c35c20d5e5e5598072cfa16f156f6d62e270350aa2b832e6110d7934a8a0816d23b1c40a9a51ccfc15ca8ab178d3e0db6260cc35912b9a5cfcba757ee1328753cfadd6f9c21e6534d9c43b44d2f76ed88437e932f1979f41a0f6089e4aba3ae054aa6da2a64fc4b087361bd161e804e98c6d4cccbff2ed57d916eb08032e06a95ca27e61751c5c088f91b340e60ccb1d57fcb0f6bb91405cc8eb66ae516f39248722fa7d0a1b58d81ad8d7b49449cde70f8f7ba52d92c9103a4395fdb0e6bb8a6e0c609aad575bb614c81e4866b3245e7f7a04b539a2af6fd1f075b2cb23346503bc96f68649d44e876f987bffc1c2ade0b62d7e50b0c2ed3d56078a16690ba0bd59738fa089430f675d3ffb0465bfcab44b23d5729cc09fac9b863ef317c72d5cdac9f61e932203cc6488d61a962d6054c438f83470a882877e1822de60b4ccd4350cb5ae231acfccd7d2f0f09c761866e469326f4f59662d95500c01e9e16509f896ebcedff0607facbc6a033240ff0d8b386f12e3325e047999d341e90059a51519eccb0fa241f8070e98689dba524cfb7d971f28ddd703f32ad91684ff5599e3d05e9dd97a649874bc2620ef25eb2d9e0e7b0b282834976e662fcb91578b9d3140c04780c0a05e22c55fd799db75d687768d124af5bfc436b644b0afec62bf371337d36290f51974a949616cb9c801cac237ee28620e3e9b2e05da85c681a2031cf449988b28077fac6b88afc172e9717d79d8bae372e7cf37ccfa53620e725110031b40bd88fe9cf549630a22aad989d92d6476c156c9fa794ff92ce1eac795bda7f7bf880ed49430e89b85a56a717f039ae2e5ef6ddfc5979a333eabcc74a156dad0f8c4980bd3dcae68f34f3d76f6c60bf80ef1e531b4101418df0ce41c0ce405709aa141ea7c211bfc4240a1aade41f5bbb0f9edc3bce3f7daa9be5ed4a238aa7fd2d6226c827e999dbf88c5720db20db5582789222fed83c24541ed86127877d17c0c32f018794b321383d8f0bf2fb2faa564e43d371c57f84694c9b3ef3162d93ed884e3801831598add37a00a55843df8b508208ed091bbf99e29a4174e451c8a0242842e8e0d4360a66537cdae6d91b538ff6606722ebd8150d5c1a0c8a436fc239a67d4e090c0d8c06a56276e84b437751090fb3c881d3bfc170174bbc3b58175804ed1c7a6c03733d2c716aa5239614c8837389c5a01fad54db0020763640662b8afb3ec2f59c5d61cc21d67197b3ef17d16b1a563c22f78d1c2f79bf2fe84efe467261e9a932322465ab733686240cd2a1e160209fc1e743ee1fa688452f3458422437194a0979b6032c5b9b87a0ae5453a2991c99cc4e1f646429a531ea30a3e2cfedb77cf3b12a5900a1ec12a48c3210917ebfcb434655ebc06a4fe77e5fcbf6437224bf306709f995e3b1898d048c0de7daefe79b3f9abfd67e407bdbb4e340e3f4f5545a2099999fe7c1c75bda46b76b41e81e9273bb2dd4f0512c199d4862e04869497eb531aad9bce0adc3e696eaab164381d0500a2d1a08319513bb0ce6af9f1b18fbc6f39638f154e3fb9c8d88c50cf0236a04dbd1f635969d0e8c71c5779535243ad497ad89cb52a725684307ed81dc820534385328d7994d5c12f6e65e2d5bb9e7114e19b0b20135b6cf40ae76adac6d446fd5a55801cebdbfab730d3dd11aefeb93373bedb983318e3004db94e5a65dcb80c4a304655f4ca7908a2c26523a038863e70204d4d758179629adf1ae875464ac5bb59761774b24d467baea1cd82c6ce41a34cdc1332ee5ca8ef2f7d4afe829ccacd22f8e6aa8be098394d9fe63ee065e7dd0a869e7cb1053b126d6c392726521f244285723bb547d0f8780f4b2c84dfe24b5c7e3417d81cbc60c5225ff1bce281a946987f80ef393c3212ecaa7a9ab6969d5853e44f425a2a3d770d21881c5cff135d482e10004af69bb92cc272a709e60c3368a4eb84ba72fb490131a79166af66e3c9e548a2da1f9550f405ac81bfe92976b7b648f6c843031f4b5c82287d8033cabe9d3b90242345d575c83ab2cbac3dbc8dc5e7277dd4c75bfe020f559a71e640bc495df700dea7092a07e2f2985e0a4952ce9e1b28ff56c3770fd29d44b09a6aa81b12d02396aeebda79179be345a40c1beaab489056701723a508a9b8f3cbe16bb2120f944b70bc3726541c470c239269323e32a4d4577ac0bb0914bd741a5c00994d0d6f1879f3c4cefd97748d4b59504b898882602c8c009d1b959fb56d2f88fdd05ce89c7fafc43e5c790baf46365c2e2876f356745f623e7566a1013df319508f7fb4897bea01d8b1f0a179f22c95aa392403a628929173ec2bdc45ab566d125af815165ad84bfa9a5296fa1827557622387287cd95f5151a535a5e4991fd4df7f72b9f42d7d29bfe4d44d007391436c08e7c4faf9a8908569efdfc433ee216f707233330ae977ae91ece90521fb938365cb8351c45091a68cb636ea703dbbdcd8af465a34590a543c9a3027190f440e4a997a609dddcdc5084dca980f7621250adac5cccd5c3dbdb81489e8f33a9e63e4afa021393cb4885a872190113fcb1e76f5126a0f15848b50126ecb7ec6ddb9d672958b942bde2e43096d26b4d9773113ea2b6953574f07e6a2e3635b811bef9a285066a4df8feaf1bcc9e29caff6130058b588d4a5f668d02cc4f0e4bedaa2d68d7aa907744e976faff1880823f3c6562ddeca33c8f1c138802351edd70b0e79614164d60cb4d612a798dee5955fd4243a120f7e4c8cf7211f0f1cf14e060cee0205f8cca155f983b98593b69b961e4585ab286f0283a73e1b1f45ad939451a7a51ea7aa216db86645c077fbde18b16fb6a91e528c99e4248c5af860ba9da683c85c3db5b5cd9068c2e865044f05be5d02a5ef395a907facf8ca5d3932b259d0576b33711679d60b47e92595597ecbcbc1a7ba70e5a6f44ddffa33d4477473ad414a024174fd945a06472e56d6181f97a53fc8db104cac8263264304290df9195ade8b14a0dc28eebf0353b05f45d23dad5c18c70d4a55cc2b47838dc535a159a8f993291d8f834d292bffd90695a4f1306d6147946d39afff5add03feb5555989cef43105c22e25729dc2397464e2731fe3719107f1bdf7fa22e87902991d04d4666e778bd82b8921ccafe87e62283691f964c8d571debfc5c2ff88afd61dd1eb4d659388cb7264d46d4efeaf7a7b7254d3adcf4d33a1434993e7e6b0a88180dded7f8c982ba7b94c9aee1e1a8040368ccc72bdb8f1602bf466fa87a1d3a2fe4cc105b51fd95e72e75d5634d8ea1a3ce3370df2c1717bcb57557cba8c7ed5a5b310015bc379444845ae67a034e0e8040a4fcc88640060f10e30381727651afb592bf5bc6b1f07d7eab44e28321acd487c6531d0e372fa0c08b61fca8f0cfc1b4545d9ef09b17078720b640cf31c2f5a5d6979a102097b7bdf51c76264d7ba7e75068c44e88f0ea99044058045f4b879e87840bb2cb11d93c04beafa82e1c059168e4f53a8282286e362028a45e66bbdde19895578354926ec0b8b1b3749528e01be312cf4626a278ae8c751bab65deb574f75f16495d9ad7bf58c40db50892455f32a772ffeb63783b0a4222af597da520b978b8536485cd879de2ec1ef9f370b63da59ee5de60633b8cc0e71efc51882350489151ff732b4270be99aa1f51b2296bfea966f20e5f8a438030932002e44268217a93dd824e5adf80eb3eea41fe7c7b12e5931079d70cc72679156b521f08fd4559382b5822423bb794cdcb3a421dd0cf578498eae5f327051682027f7a3e7f4428c4e8524f032379d61585ab38cee10e83007f27b8d8abfe0ae12d82f6b53a693824f0d66bcc68210591e1973dec84025a5cbf7e6c050b5648dcf57a16576192cce1545de7a5b25a804153c5030d0ede87981adc87a00f6bbb9da9d29547fd00d92a92f7a096ca934ad98cf4bbceeb06999a9ef4dbbbf2739bf511ffa73935d05b6d5dd794b23b56925ddd46e0b275e7ddeeee03c0f379cc7abe2d37e63a7e149e1fd7f6e51b58195fd637a5be43dee11ea78363d45ac25d8f9bc5320b60b5dc282f64654fdcd520afbd533bcd3b09244bc3f398cc706991435b30db2898ab21816996bbe6f5213a64e06e1073177a4b639db56f15a5968307a601213bd3f29de8415c355ead56f438028fcfbbe1e783c581b09539c70cf4fd2da4ec8059c2182dae9864f9cf0f787c7150a775587c5e9716b1ff13cecf03aa80c96ee54f2d6d7377e64d0730d2ee24af8de69175d0bf9f7448192dfda3e95e0928deba3b280c2944c91f1d2426a4a2b69bf6f26b12d893f7ffb06337a165191f8fc0db07e5bb54e31284550cc9e60646870f9b3b793b2bb1634ff0dc5015be791775793129f913bfe29b1680cab85857f3b10fe382ee3bcdf635cb81e48c1ff6cf14d82e9d1c1eef784e95a0a1df451ecae4ca3dd245cc2df836f73d1048f48e0ea0912d85e5e54b6900f0ff364e82926584cd01217f995d455ffa7af70f2e0fd3cd28e073361eaacd7bcac7628e6d4c8174b000fd168508b69469e3e1aa235b78155b9122deae3f54748d2ab2eabbc8710fe99f8c79b825d9a711a0e024db1ca90913014430a4811d5a33fdeab87e36e23e8e6f96d6c559f254c50872b88e3895680a171c65929dc1e587b6f290f05c560c2187c03418678b7a4ef27a09e0d74e270c409658991363e4464fcce769d6ce4245d383679c1346f748d590376f2ccf422fdb596ac6f1133d914b1a30d6499e6ed0d6eb1b5177790847d8f402f6f91ee56cf9c43c5b6073acb0f262a16e862bac2951debc7d33b2f33d24f0f0eb7910d53312cfe078ceff4b31053c8e7833cd91c493fa410ba02e0815ae630fa15ef43f99c1b088313f2aae00c8c2c27f95c165a16f40ded333d4b0eaafbc33c3465dba2ed1613662654978a0e03090021a3849d14038dadc5a3ebd2d4c8c472fbb0f1025ed44672670ffe32eb47fd4bfc3bb3f09cdbd06bef462a41f37827da8f8c4e06e4da8bf45222af4580b22eefc7bbbc9a03af27e70b9d552de2d551ca8c8daf0cb0b9c64089dda983249351c06668875171c6b00a44650a4dd91ebf631db679417873affdd3ac1d65583ff7288fd65a86e2e890eb48c2240bada80ab6c54d4be10049f693c5137bd11b9da177af0f9b5b7f90215958396e6371464e67ad98ac0b0992780c4f979e1a6f58ef305d2951ba73acbe8604598afbfa808a9dda43dcbfc84cca0ab2ab2835f45cf519632c37b8306b99d965bbdd2122345734a319e7733b1420dd32f4e5a5dbc44607b8f4677ab8ea8bc8e37b442729af55893506e920b147389c19a6d180339c27189a61ed31b6b882d0f19ceacaf1bb325be6e8e6bbfec2f7afabfb67bf4d36f36b5cca874473eb9660268c73163278628c93c692864b1fad97627e9662d7a8abe8ab907ae3233351e760afe5fd0d9bcb397430a12e8e699f3f0571dc90d3acb575f61e1c8c84a24970f87d646228a00113d3d8ce8120f640aa18d6b7bdf8adeeb46f8f48f83889250e22cb80aae16c246ce4aa7db26ead1856f937f126e908566505f894111854beaf98dd50ae323d4d86661f5cb0e4b046ecf2a6c0d2fd339816c0255a5c5bfb7ebd1553d4fcb592e259df1c989cb3e2d152b9f97f524979d24c42e6b17d6ecca32227397bebfd5f059692aa9fe52c81d52f477536918ffa6443c4359c00bc620cd71b9e09910ee4741feef9165371612487dfba00e6783e1727c0a6e53b29b5175dc3682df4cffdb84301dd4482ae98a75b8ccc727b057f5569c72736adeec9a509c88fd40cbd4d1b5d4575db148e9044ac967901666d63c309433427d9b271b8a21099022095624cf14928c68bed0b4b2b1dfc81462736e0295df67f110b26dba5bbb55f40ea5ef9b0d9b8c78d4988951cf2bc2f6cf2f3457a2e965008a342c327e6eb2d1367f1830398cd4e7c75bb2f14b474115921328a63390113624f7d3e8b09beeeb012d53e70ecf874bf405b94bb4abf86c6d153bc15f46783a48b9b4fd1db45ba1a547cbf460a9fea80d7e0df61d2e81cc2c1943e13b8947f503495e33d9f26a60de6599a975e4969a2b8fcb2269dcb37789392520279aad0d168b9bbb22fcbb8e6f7788c2c74f109d789bf3411d6ce07e929b8d8e324ae975367d61419ce5f0db8aff1717cc4fcad53bd30cb8364a84dfaafbfe91a02f32187c0df152267dce0f5a739fd14ea8269cd42fc100b5fa343226c6f9c07668fa79bc47089fb6cbe86b44ed0807ea1b0309193481e55390aead1ae983c7b67e210a9ecf2029904325307dec067ab4a784e2c7d05b8ca91effe8c06639da728c9fe87ff9dc45649c48e7e433e8fdd57f2c5ddb48c9094941aa62ff99380d175c74643842ec1bf0fa7b9a27302aa22a32b2096f6e474ccdafed3449a46e37fc224e2aeac91de4c94e3dce7f350c5db24e6babfb25553fb5cfe7d4e641ab09e0341d6f93595c9ac688283d7fe463599b6b8932b6789684be4daa43cf39a4abed55bc7392d1d6572695af11fb78caf3a61a724da21c83c0662f3ebfb0087da0ffbe89bfdb259f40107ee06168624423cc5d9c04fc29b99fcaa09b35b81e62407f17417d73cb5a4ae60fd3084ef7dcdf3a3ffbc562422369e0e3252f0ea8f915495216a3c5149ccfe6f6dd9f64cd76a3f321e6ec166ca6eef825c3ad91bb4628d3e36a684041593cada07379bba7697e9894edbec64044f2d74e6a598ad7dba4a200bddc0529893fe4a0148b35504acf812e050756d6f6566ca23f1b6cfd9428240e57ca5bc8fb76a4e67e863ebfff179ca90bfc27effd827b8a0ea5162e26b1b5f7174c008a7bce1f6f3b32eb2f74bcc252d489815823dce28871cd6e72f1de4577f504a247aca065d686c15d4903503f1bbc04d4dc28cc64f7ef615e3f78a59556ee478ee495a222a461885c971adf63020eb1fa2121e9c1b2b89fc0e207e59b2990b6d33bacea3aaa9c93c706da9c97ab3d61bdf122df49f9728705ed14c3b214d1aa8ba27141a70e2eadef999e59e17fd96b70bc7bcaccb87948520648762b3d5658ced71631f3168185a7ec4e38741699e503d5cce07a2c6db7bb923a73496cc24fcf3b61f9f98b9c65bc35636c906b0a0f3b16f4fe8c8998b858981907796d714900d759e3dab14a34e56809238d2c957fee4e4b1b7c9b4ce89b51effc28bf40c74dedab155e44e2a9d8416cf6f21c05904f2262b30fc9e03e3c0bdb61a8f883e69ed9ac4b7629c26f7d0d2913550be8790505c3d3b44b8f704ab7edf8b243b4ff95f63d464b23960283c4f23e3299e70873141d72f383c51673ff7a8962a42fcfe394b5514bc043ab75486b2b30ae18fadc04a7cd242ba58026eca357c8c4231f155c3be724cae93c08e81e1c6553fa1e07b6b3ddc05c80bb604c9cc1f42da6949e84841e17341e556a40936473920a49b2ac8261eae8d99ba583f50814339b5fe66216efd7a831b8cfca989d7f05e2e4ee2419dffccc0411971e033e4624f7fa494980e8a7708a68e8ebe69f5b81bb77f9fcbd8528fd5cc17f34190dd440a5490863faf7658232dd6e5f9d1717dc91f0d2fbba07b6842ac2df313774c2381fe2d33733090b3bc28c525f221c9ea6a5331e9f7e8855895b92598e25e6856f1abcd90286c8e4277759e465aa3cd1c5f23eab96f4ab0ad3dc0db4dfa3dd8cbecb1953ef9b5bcc29fe4bc5b4ed11668783c15b94bebcb14dd944ee69e3d30bf4afd2640927eb5476796ee4088e6d12df496e2a2364d2c79b5a00545680fa5dd5967cdf5a063d7369888a552c2150c0c4c079fa056e41ab07d763a3e9db90fd2064537e35214ee569b57f67604a7bb5cc36ca6bfab32becd630aaa797b27a4e0175489dc19f4b676de4fd4bcfd86a4c0cd34acd72145e3f73e4070d68170f303781e654eafd02a275a6d6bdac2422c46521eb83ffbc9481a4af55bef332b55e4247b48f62954357ebde172d7125c61724bdd421675053ba31d3d9b268de919fca6201f531b575115ddfa4ced3ca0d8952df1d004fabafac51406b9560d6289b50004a2d74f58e7f103f20b4c7ded23a92d41efac84651f71cae4a8dee0cda71f31b724f79d6fe3f7efb5b0620df6839add194de66558396d9a6e256db8b14c48e3b0ea61332154783cdd2fd2578de20053d5021138515a4609a7f108aba9c1b900ab0bd72f1df95b911cede6ab2494bb197763835228a957eb5eb7969c26ec7244f007585ea20f8f4770c57cd6cfdb0a95517baf1c976ade41ad678e439929677dd8ef294becd3b8e04617f3a2efb4374b30eabf3706ccb9ec73b6761d28c3652340a9c9bfa093b381ef8973e482025c354df12f0701898c584e573394871d8946dc049ca68fa580870a2c3c8debfbe15c5890a3bf05dcc126af20aabfbb556aa9ef0ef2fc7646973ce9609249ad4150ce99e211735c8272159997c8e4a51e77ac8138afb28a1a7137bcacc42290ee33c7027cc146221adbdf12b960f55dbf67ff58b7818a0d186e096bec4a1b97ed11a673aac4c88ad3d8c0f0bc2041f348f4512ffd36312e6712415baad4dfe658bbb8835941042a26bab83e06442fde93bb8eb8b86fb7740585b9ae99e45f837c5dfdbd70f092e27aec637985842dc8684628c51884f78f16f6fc8316ff4780a51c26665b11c9f97a68c5b910aed7ded4ab39389f3a69b904f4760d287f8a6de39a0041da895e2365bf9153a93c88603102c397d4a12a9ef42a41a7b3bee37d1814b8906b6c9b758cc7625707fe87b12817ca0b480a543aad0665a4b73f417a98179394689d0a978416c538bb83055e34deaa5c3b5b170f99e07ad9349402adfbffb07e7879055a82bd229ad1cde238b1f1f413bdbc07f6b737aa57445d319a08662fcb2399ffd8211f3dde6e83c0af4df4fc9f77f11e752bae5cd58c0e019e19275836f665f515efe81cf52014e48e892abafead39a7658d60350e30b0cd519382b105d830e10067f34c62a83ec97ae11328f803bcfa7228868bc099c304fcf873f70a4ff532185b7654f8488497832b64fc6e7a63d5fb72d9a21d609b028514c1d124531cc6dedb63d3e737c07deac25e62fb01ac27297db5f0dee8b2aed13d3786471a82f1b2a73b9e6200465262db779522059b59483b5e7965e8ea18aac291290d629216e67c6f03f9bdc61a26d45ff2b25bfd600242b95e709d2043bd3b3e520aadf088eaf86b02febff106fe97d7f0fe5f67cea82d7bc6fbd64200c54bf263c5cdfb2e122cf3576fc3b600f5ccbde91d46e96945c04dd4c34f418857b2c6fa851490018e428292430ece1d9d0419c9f0e37c26a5a86a84903d87f8e46a1ac8776feb9fcb481f2619d1a4fc69aadc13cfb0b4f5678950b0a2e7dae895e67e2ebef47b479d6a561c6bbb291e1bdfb546d23b497de2fda1e87e16d8bcd4883ff06ce6a0bc2346c1895174692e5b3d093d8e2bbb590e4b9dd897b31f62625bf074bcae95aebe4fa0967d41d7e046d86574e15eb671ad3564d10982d5b36f92b092dbf501313db3fd19f9304724f13437e8746ddb98d83cb25b6cd2337193a048ad26b8f6012e683216e8ff05839521e4d926556f128b350fee2e7e3d7f980aa8c3a4c3441ce14890faf4c2702e8dc5793451fba35a6627c44e7f4faf34cd0b98d8781fdb424365e82cad91c817294f075d6ab994907f264e33a0155e84f06738cd180b3f5c8b3434251bbbc0dfc61e370df81e503bc5d387b1deea18b92ce0dd36cb358d07027fa95b157727d48789c872dfd40c87c5cbf877dbfda6be170e61ec3539dfb7d9ab78b3a22f957d1e5f7b00e777f0396c95000a5c94745fa22495daa68b432ce91cff813be4ac43fa036ff1a5b79bd1f3c4deb9d388e48b5c4cbe4a243e96c69c504c08114f760f735d1579e599079515530dcc69f3c7f827e22dc6dc81b7a7af572a0d4fa60814d9cde1738a10b8f0c4550970fc9689c69f6d9ff2ffe0812d44b41cd755602e87799cc8b74b2e06da12f34733fa10e00feb8769d973ba4676787ddd59579ae70dc7160e2104389574a16aee69ce1c827aaa7ee6aff208e8618c2b181e5ead974178ffaf8dc8872ad707fdbadddaae989399d4418e47a204eb1cbd32d48d8ac4e9ab5360b21df005157ff95f1bd8c2e0b4b299bd2b8ae12df8f9aa13e3d1dcde9035913e370ffa5123213511aff87012a7033f0c0ca2f78cf19065a5959bee6091d048333b9d6bf5bf176872a8aa2a582c5a6d1bd4d4789b6e99740e2a9fcf2c513f3c1fa439583e3002734f6367f9104875484c41a2eeff8f1e5a41caec532795e3ad15da54ee459d621c7d7e0cbbc5faf71869cf946f1e9b9e261c9bb82ba8e222ea33a13da5501f017364f3e107954519e0aa7b8f333c7944e48b51a5a4ca59f8e0907c0f0e1194429a72fc913dd2ab76739e2f4e5f78d424531f054aac4ae7772c6b505cb34e2cae2159bbcf1781c8ca6cd648d8f23ad2ccfe4b1ab7ea19f1c272a12016c0d106675c02d8a1a6cec0f61cd4cc9adfd28aadd2ec2091d0160d35fce87950060cdb19591ca010f14a9e5c6d5a4ea2a72f29f27808dcc20157da4e1a2b278a1794585e249377d20f66a53c275de51b16f7304d9b2473a56a15a382dccc78c68f5c96afd83001a776ddea776cf390eaa48995ae5a24cde5377d96d05e0f23b9b6872b3231e1233dccc92d1339ed4990fba54ed5000cb1a696d2dc197c6ec8f6edae882fb3c580f9841ce3cbe670f10d317e4eaf93b614c7e393636b000abdf1d7d8afcea1a25c71f9ec48b52de4cde94ec5962da99cb7a3f1150bd42a30fedb8134223c0d4177f1c2a16a684fc04ed6fb53ae4e9ba4eb59a3d83586e451f665a36894c268aaba6bc0b1d49eca653271041a3538e74948195bd12ae05281e04a80f08abbd8f15e7a7ed578f0b8216f39e7419576713394ecf5e83103af40a5793c3f6a2fd7815f258d0a6d3065ed8cfe973d2c5ba74ee2e94875a89cc682d32888344dcf53a6e4165cc61d1b86fce653dcf5e28362baab0917f99f0769c46e95235e4de55bba162b9e995f28f6a977be92090801d305abae320fc7af7154174990acb2b37e51f1c7f39dbef46466d3abceeb24da17c466893041d81049a4906ded3e3d0359f48425d51665ab7365a87381396c0326d6cb2c648d4dddc851007397493108448c8f6be53fd3574396511f97f37154d956bf9bda18d5f072f6944972e923bf724d17dad26ac0856b4782f0b2c80a3a2a83131f489933b8132d2de875e6d89c5c16a0d9044f05a947e84ecbc83458e6bd5e443f06b04dd7b7affec1ddb6fb455595008c0bed1246afbec8c77a6c03e741da435bd58c065532ff0173e9f018b4794a8cb12c64a20d01d4fe1098552673d150c2e6628f7578bedc0ac1d1ad7b77e451d160ad8fc5763f9b71892fff6bc0c2b7afc254055fd37d61c2b40959e954b9c4b3bdd11ff40ade64789df8591b0fb1e2c7c1fcca3283a51bf8505597b86b796e5181debc79d0fb577a3d78ed00889849753031f7242e50952ad00edff12c2192ab61c025c2edae079535e9b864b1aef420c4f5e08785ce004783aa5f061736257bfd26f83e6367943fb2032dc4599306c9b7cb5d644e16427b1a7f614c27e772b807f71ee3d227a327635ddbc87f48bc780013b6fd64f85b4c53b39e78ac791bd5f82dbf5e321dc6f16009cfa0d67011c129e2d254110b8024c11eb79c2e19d0e41c68e64bf5a9c46f19abdb6706e932d2ee252f4ba5f02cc47eb86e3b4997dd15e41166ab7762d85b81943ff2e65089b030c60d84076ae0f7558567de4c103ca0077a2f9ea056eafbf4f5c3eeba97a1b00f117040c2e5d3fc887b27d02b6e636c99e0aad977cf168199f290752cb95236480463d2fedac0066687c4537e11b42735400c7a39545beaee6dfaf3f52d235b38ccd05e9465fcf51a9283dd0fabd8b2852ec93d1bf0ac5ec6180e71c1ca64796d8983077bc439f39127b44935e1c95d848e479013ed05296a4ca1b44abd9e20c05faf1b48a9e06f088ab5703f65b0491b46354efbb13da45db54234a7ca2377f4c638eb7e6c42beb830c2b63bf0d377e295ce309549b268b3acf83fc5d380807ce0e71764da2e11f9a86524be9640ac9517040495ffeb2e1fadaa92b24c9f0e083c1e31b2d45e738043d872d097a49cab2f9835b8441a66abb81e5bf69a6d142d77134816354303287dc2ff8993c15383e7dda5d331b5582e4bc978089ff5388d7a38a0125951b2d837dacb73b3bdf2ae7554c76ee98ebb47149b687066a6ea8dfb561e7a65420766e5358b95fe65efb9d6571d0811a29fffb85b3d3f7220cad8b5652da2045a5fb8451bb38faf44b0bd55c24956e580ff92e99afef930feafc6ebd3a2e9811d7618671e23fb455c89d59aede93af2d3d4a3257873a8e10c84f847d690f699e0022b2a25b9826e5a7c808f900d98baef6ae054348d9ffc58e3325701094da0e89258609cbd6fc540ab0dca3a089a6365c1592f8b6e8db1cbf730329e3c0b8cc4d3f1f3631414fd11524369e630541df099773c454e6675b50124910e8626c1ed094440ea188257b80982240b79e13170ef5b7d406fb753752ac1e25aa634ffd4314988886e429277b42586c193bf1afd3f5f706e19a2fedf48804d67130ae2ead29a7248c6a1513b4be64c1d966d351f1eb3f23d4800aa04bcce059ab269b59585f8d3f499e566239d4dc98790587afb19e5ba35e4e064ae490abdea642a2c308d3b2ded034e7c8e65b6e2d3deb5e413ed5dab506bc0bb0f7172f3012a5ea0dbdc841a9479943508ba951401ac9e595bc6dfc6bcd9c5d9b62f656310de3ff2b139075557768600a0cb47730ea368b343cdd9e2eb55b07bc44c8c41a5eecd066e4334a5252c9bcf19a174e6f2fb1ed62fa854ed98eabc52acbcb9989434198c1543ab9683e7ce57d25ccf604920483e16a4a5060f2e13c1979f8ef117105985d0b971791a0799bef99043b03124b5fc279574d73adff4e72ed7461281077a5180dd96babbea211e136d098b6ff58defb2bb5ee7c0eca719d0246aa5666342d2c5bed51be138697250e98261e0a6804826d190a66d1fb01e28aad8b8a0a4a494486bedf20c6480a5acc785b9480521c570018feed34660a11da64a66fe591ef841b2a1a89168c9cd38d3f1a01f30c9b119546ceef278e4cc70541843168c45b367df5877821f51c46aac7ab81fd5510ad3056128ba7db1ddfeb395962b60defdb043dd768d9b32b3ba86adc74e5bd9abe72a1a157505bdccf0418ba47f8b4eefea44d69f624b67ec886e5cfb3ddea34523b9d426e16f1fc6683ba904f5a9b9622f214684cbb7b05eab19d4e7d082dc818d73964d052527c1818ca855b265c87862f4264967a026a01bf8429ef6245529226ed56d91ef8bf16e38900e70f5482ebe3b60bef61df3f69a352ddb3eb8d7afc706a702bdb129a78399f2b444091b048babba548d1ccb5de4f81fe5a7b6be5deaf32bb1e0fd398cd4166e6cca93003243bdaf0219a56b5efa3581752ba3d5cb0655b4ab87b958abd367ea97b73d6600ccf8a26869788b9c94a2ae6602bf8939691bd6c74b1a191e597560f8c6423fedc09c8a964da6eadcfd78cd0b403278b8fc3f783de7ad6852ced72691c26a48626e399a17ab6444767edb87f78a4cf7e3fc48c1ffd2619e1ad8da67dce0de0f21210960561c7b8949e9978558b2de9e426e75b93a65c00f52b3f7c8a9351ea9cb8c512b99fc5ef722fb8dad563552d00108e74c2b39ec2b83e5531c123d28db15def313f5dac4c532dc51ae7bbccad0982aaa43639049f5889f3e5be312370558455fefe99473dd7b13fd8e4be2d0eed6383ad78c35c3e164933caa194fbe1b7bde558c3d21dc4ef777c8b80e0aa795be4d5856a05e4ca4262f3097f36841210aaadbf4900aec6291c8f67cd915b8a4ab3d6dbae54b1337fdf20cb64cbd5e7caa349a0294ff3bb55d89353c03cbcd56938edb9229139386c5317108773cf617b99cf9030dd521dd351278ff3386c5b3d57e7164f07914e1dc1c6e4f9b177fdafe4aafef9544bcc5f314ae1c4084f5cb5d82fd3b468f943dffa4a9945bbdc4475d57f8837ffc0f9680d5419af94c2d6d39c5564c784ac10da907c632e3852632ec9508e53e47607da1c0dc852a92f52040639de278dc3fdb915f887c3d413b7804fdeb8ac71bb379a922cb6bf8f81607d866596e798a6ae72c6de1a3bfefd6e9064094b62ea48653c4d27c969709e0c50bc457f2c306c5503a08bba99d43c1a211e19cff5bc16645f959c74722954a49d7be60c5ebaa99b5e8cc1cc83e28e4445ad8b75aa6f3d771f2b1095763122470f74b3f416cc25c188171c2a277d6d810761ce646ccca61d9b4133a98f3fd5e8070dccb24bebb485a2702ef2de7d1406b8837f22bf338024fc3e8926e08c1a7ca0949bee144286e1ae87efc35fe021295a002270f986ff72b6eb51735c9a7c53ec5579bb79313ebf8e6caadf4c5ce30e7dc7b0505976334789b520a14450a8e2ffc06cd2a08aad7b95608c5c18d2ff07a7f07011af1c666acb2ff1a497e549a7e53d428ce564d96f6589d323f4451ca774d359bc3a92065f3338f6f81424e1f4f471ff451538864560b7b69da2e4e3aa0e952e43f0e688794cf792633fcef98ccbd8c3bb8014dbc17cd4a8607921bce323d2442b5c0325c37320c6a3a0c3627a589b57204975c06e4eb97ef670e0e51f121eab5a0927c5eab6d82092165545443a3a671ed3bc730e17ea7f0b3b5b6e3d22d0c11fbbb137550c8d2b667c87359a8ad067bdba57497c2da6a785095df9f33e9f97774c353c0bba79682345480ca9e7c59733feff8189b93673f3e3a2f8905fccbb6da3ce17dd7951ca280da9eee447e7ec7a6f0562fe6178d554a6dd93b247e1b05ec322d43bb6c50864bc138a4cccbc33bbf7f093326b63a0a29a01a85480964b606e5d555e096ed1fd24966e887ace4fb7e5de8d4b892ff209eafe37b8061f8d6a02be9fdbf7a4cbecf01e2444434cd085c11d586bf8694bcdd5aaeb758d81a39437871a06c60462a3990bc96e380f2861b396bd8a221ebd6b16e2375c6317367f562c2cd06bbd34b7c0b0b4d6a10e74a3a15bc9b58bae76a7b4fe3c479f37543372da7ca347c0ecc5273c9700d3868b3886d7b7a9ca290a9e41b314acc5d85a379a57230210b107a2f6c79708c3f2ee7a41a6af30b52e2b1f5686c60fc898dfb73f1796b5a71e6bb0f04ee29b97e8265c65d35374f89310e97a7d85dc1cf2ba26e20987e1e00e8a35589e88c9b7a20682c1fa0b7dc4d73f634181421e2d8e0b2cbf244b27779924864057c3c6c9217b5be5460690ec9678c7d518dd3dab689c1d6cf07b515ac8ba26a0855570565589ec9d3ffffebbbf4031e569341d9a5037e97e672ba0a712f1a0a6f5dd7829e174fe8e0f4f6b3dd4acc8c28d2962cea5da3abf173d8e864498eebf7103c648baf95bff9665a15de983dacda38ccd15d13129286ae2a70affbe3804a2222334d32783758214bb3832e4d1b3156dd70038714bd151c1587473346776af62d1289ab8ab3432dbb55917c51a361ce2f5fca2db65f1b6721eb3bab006a5533d4a48df498d35927bd648c8bcf965c7761d4d43b175d739a796f44166071453922a2b6368cf5202230d3c317f93a2b56953a390cb19c94c7d3d796ce6cbcb2ffdc36e05a3ced74a2d1737f4ac1707ae28681c59d0f063515e35c6c3764ae30323e65278aa9f04500f7e45f3b89856d1e21deda39fdc8459f1c6cd1460863591bfebe4ba2e3d1df9afd831826d2418f43a7672604f7e9105dfd4395be592c87ef07586ae9d25fd41cec39e1aa3fc4e8d74ac7a1c8c1d048bfb8fffb61d26578b8d757e4e7ed2ceeba5ed0dc1cda6cd12167c1c7ab0e35b4062a01318030fc8b4cc74138c079109a557b9402c688b9702eb569c42196309e24d47950c9448efbcab7404443e52e8a08759a80b5d8e45f63224c2fbad681afe4b4178c29e2a1e538774de4da71a46a2b187a54560a11fd663b17309a103f03b1729510bd0f1e49bd4cb719aa8d25178fa090e19807ebf662026543e7d6fe62296b18df71b9f9183537d072240512dc710dbbe739e10347398a06d4f28652f496f546106544b2a09cd568978990565a5b77485271f9abf93f03a1609701596510ba39de73fd8a300304e9b1fd59b3f3bc0a6719d3f4cce3fd22bcab155432d45a71fd80fc101346035a4835668e91f99aef50e5a1829b376f22d54c73e229e7a654bf09ad4a9941d8aaa6b71882e439eaf24657b0d765c343bd178bfe60dc9bfc360f067087963891ad2280010f68e789baa346afb961cec514c69d777e2306928478dfc9787e8d98ba7aa8360c803062927abd6bac5d8632b4f924f533101a6b9ee2bf2c4a44b4220ee80364760c481f2e6398687295fe9c3ee38c16eb98f4a4f87fb93e4c8b4f31ef8c10c7ac54560c23c12c47f0c9b39c52921f6cd98925ba6038c741a807cb10fa5955b2c6be3e96ae229546bf814c947b053b8f371b27537e2289189a124157cde0d79cfd7380ff165797999bd45650e99e1315493ba00565826cc376d22e55e989dfef7b06919c80ccba850f5ab2e049618212d03d027a07a8b98d4ffe1b9fb791bb6277647afed604ac3f762b07f665e3d3bc06093c42063de8702ad9dfb149b4bdc493a17f8897876e962218e5a68073033dc25b45c7b0b06498851098f2a95165e0f4bf72b245c97f995a446d261bad4f6fcb83ba9e118e9746999121ded2d299cc9b1f47956f9cba2d93d53abc650abecaf2974b745cbaa561379a2c49feeaa00fbaf5870822da6348f2b656555e4694b6f3ff3a13e8b0b6adf27b6a074b0fd2a4daf3716a51ef8938d7e2de4c4861c4909e595578ba2e2ee5c40b89362156712241787e39c08c9bac8830754c8e793c4cb6c6b6d064d7246bffe1c860c3ab2133893b78bf1bae464e33c75241a110e6c7d93844d7e7bfa9e99d1c9d6e73716dddbab3c662bf2a502b4c6df04333f7f10619c8f392db2c343f42eb1756a1ec814f025d84c760ec78abe465cdede4763b5b07a7cb2a9e8f1e457b73cdff400d3a8dc297c92a1061e5a3372a87ca32da4a6bc54df6eb1c26d6fc72557967973c38caadc56a528fe8d8d7dc6ae9338bbf86a30b3e324f5df8b4bc83066855071d7ddce21d5a1a134dc6fcef2cb579a0705847c2833f270145fb0a799e0be09f682cf7dd7aeca5a3814cef57f55a803e46f5931e99cf62c5ae4a35a86b87ca456d02d2aeddf10f7908e69d872b48de6709c6bfc04b2027341166add8cca38eef12bcc86cc6a8b521cca9f5752c2646c0a68daaaf18478274434be7a2e5d22f2272df265c22c2a7fa0a985cdbb74b2cc6e4fb47ecd9eb82b4e64d5489a0dc76786d46dc447db092025595dbb00f55114390b65b456719e6269633650783bfb8668cc171a0c173239903841cf6c7431090d0442cdcd3e55c99722a144468be787209ee9f4a761f8748b3c521ee91a7c707499262910aa689f9ce3702e0a6fd20d541b49142f90eea8f58b3c8d8a3dff9e4201ec1a0f22d139b595615f6ce505f86f628cc04ffde055c90689d686bcc95ec78fa8a97e48cf93825f92a6acb315d075a2db54cd63840cc955415a10e114410931bff79a8c4069d156d7de693498d4f9fefcf74d544ac27ab46b6dd57160db61ed28403f5778d9ad9ff052319e301da6067f830df87435b7576a9124823de44f2f4b9616be7b4c81df995d7bf2710aef879ba61b5a7a1b0f02c4dc6106848d25d268c5eeb50ef1b9d14aedbc0ee8c1ec5976c34e6b2dcbbe2776cd40420420aeb2ee2240d390fbf09170e8884097c2e4b87e2a18c820038a9e20ca11103eb095ec69a3b82ca0789ce1664fb5d2a598ba6ff6be27c06c63701155262d100486419436cd8f8f099305e12be5ac04270d4cebd878a1fb84863f0a3308596acc7bfd8f148cfe7577e38d218c78da7f680a84bbd7a33bf70c793d9e8fa4adf09ee5dbe52df9ea91de1998e0db651a1c06a50f44ea00b4573e73742caa1d17abaa92b3fce7c34d435d5f97fe42af259fd8392145b6d7027cb19cdcaf1faffd3606f54ab39cbc5a6846aeb11e605d164d67974f6a310ef81d4fd56cc706c3e9a86880aa148eb80175e727035a6fd9bd40bfb343ec31aa6356d819462d1f71a43fa3ea004cc6f45ed91ba2c69084fdaeee1692c6f2d94134a31b021e8771bb98aee3f6445a4a9577206ca89a679a3098bb90a72e9da0174507aa9238b751fd319c1a6ddd694e025e47f583283fe8cd0176c81ff077435b803a7314b617a3f058176079436e4a99d7300b27090b10e8b06bbd2d352e23c8ecf56a1ccfa5d9c7abf767fd18af8602468fabef952701f81cf44b0e6a6d3cdc7158c60a997186bf702ee1e4abdbafe2051906f634cd6ce3dc92cc86f8dd6b17a9f13f7826880e165d8899df7723b887f7f3009c2df3153b4837b6f4bcbd4b95fb17f505c863603614dc75c0607483525ef06c1c5d20adaaf81255a972d1fb1fc8d80eceb99eb02e1c4a5c3d480465202f4d86a0831e80da16b0bc91764d9a85256b08ae10ee55e3e3aa4a94670ba7d2133cd7710ebfebe84a7c423f8a611cdcda3aac0a0d31e49a36da8d39bb701ab941635cc7afc01ede9c0cf15811a388461525aa015e1524a2a7f18823c98c37de0f4c9e58acfa6d4a9a221efe916b24a02cb1536956d7da317832f63d7b041ee0b60951ed6f934f089d3166caab083b43aaa920652627348b872a60fdaaa9a8559d8e321035e5451cb2514c3dd89a1804e6e3652b13c89906d7ee7aa5a10fb0b41616f7bd76bdc7b3793500adff0ab320d42b6ec9af0389c14bb9d4dcc97a03d36a73e9c141700e3cbc39469fe1c7c75af1a37d6a776f8686c0daec5f4b05b73a9955ccbcb79cc67eba001a3281b9fe1391023fd81b607d2a02518a180912f49ca106394fffe82380a5b982eb104281c46db7f36a012ac58031cf86956210d778b4dc23ba3f74622a99f64a9170a13f4ea9ad59479bf8e38140c1cc08a2306d5525418fbb4661d2fa561318dbe67003524fd90bd7bfc3bbbda5d6e4ee2706d37c41e89a956d203d81444c9d39e73688b1c4a9c1426ca06d00cb72b56e3b31616255bf8984b2c6de23c12439255fcfde014427730e74cb770fac71695567da880522f62af3af96648db19c6b6f1097c37422782c3342faed8c43e5ac285e9ac2bc8e9006143f0911b7ca71152a1af678f979773310d32b43ac6d4ce14053c8dbea879259b99b1fae30bb8b1c712f3123ae19ef9eb22dfe4fa81d37a6ee56723dcb708bf0a0917a0a488d57bebb311a26f4100cbc8b7e673ca61b26f020aaaa84f7459bdfad62119644d9befd4d3279d922288587dcd331d873e3f3e2ac5588e4072957c32b64d922f09bb7ebbb80564c614f86cb89af87f851a4d9dadfc54bb31e4a90eb1301bec1cfff9daec3da81496b1aeb20d081b44c4181eda56cf5d424b0d7ff6cfb5d195f404415286705c7c0e9e6cbb21ecc884a50b51bfa89aa77ba69595fa4883a0f2a016f2110a6e61146591faf51237241d5722352a00ef0558622656a963a2fbc0cea80090358b407f5bb257e9c2e3c0545f7bb4697e9093118794fc0daf55a72ebde526173b346d143170eb6d3d8c60c68e18eeb361eb084fb0e7b3ec3db360f834cb3b739c6bd5f247c3b4f29b6b095cf6d3232597396ed9df60278902086ee137b90a428981f981df20ae2e70801ba7245ee292806ac236b0c01a6ebcc24fe192dd82b1143fbcbac7148d3c54bd0f3e7ede91a5d6b73185d3527c430af056ce873e456470f9e993895d6144bdb668923934998379f0c7937d23705bbace95ad3244a1718eb29dee00b98980640980e4ffd9414776fa23b2ae8f5c22a063f0b04db8b37b6644f56353bc85ed5cc192919cc3cab85353f7c7c91bf804a985b1753544555d51ecfb936229fceda69c782405514555ad67e29a8fc0eb895191a56e439f9e126b1f584427f897f146491393f8361c1fa5325bfe424cbcd131891303045b7c6cea2ea439ac07a3bde934226698526baba6abef2dacef7cf40bc35d224f108c81bb356e764f18971f378fab2a81d5b3351db688e109fc03fe56d8cd6cdb35b4d1128ef8d257f6cef9362d46788f97e317b29a141b0e7afd7a8ec685d7d0bbe775154a2599a4f44a1ccd17dab4776df58fd5a431ac96fd89395732b9e29c22f3649e265f6744726d708744c3047ead263b4ed282a39e42239b0840d1fbd28acba333874fe1332c6689717fd96f4479cfba2cfcbed1b9b852cdf922f26b09dd3c8edaee77b73902afb8769ed033ba045472ff7e2703de657ec1808a8090d1efa8876cb6f962f93723cb01ecb2768da32f5c9f14edad063318c636f9f07ff16c6bb5b715962f70a5392a4ba5c9c30d13adb053fade163bcc17fcb5c6e34ea33c9170e85372119071d0df52d226bf7eb184e2c6ecf350b71cc6594d3a21b9c7ca315d7589bad599a5ef8677a6510d0f61e241a4ab2ac1abeda21ee057eb9cf0a4798496d3cc0492b24c27ac8e978c25968a3e18e10ff5b1d6ae3aaaa6546c79da08680eb32b1e0940098b050ab4082bf8ba802cc4f66fc1008f9624aa88636609d4f9044e7bffab2340db9885a0c72d29a17077528616a17bb47f11f6adaa8f3312bdc577699b7a1c58eedceaa5e4befbdc4e3781e3d4f1af4f74fe7bbdf8df3acaf5cf03bbe716e28022e8bfbabcd968edf38f133d07bf89af88bddb7b7082e33de154a4d6aae43fffa28c03a0bf3ea9c5fc6254c1f3c0b79221e124d5ca2254ca6d9b5a27724392e9ccc8f8ad871f41916dc3d27ac1da6924b518e058de9ecc5d4cbf735a757d6f84374959e2270060e36678ee6e99af2f43a3bb070e6c5d512f3b50a0ebfcfe14ba67076171025400655e764ad9d5be3ca4cf0eafcc0174879382c44cf75eb23c27153ec73a7683bb1be144bb956216bf989af4479097232a3e3641bea547b5d69e8e9b5f21c9605f2927caa43c58e0242d677dba88dc2f8f776ee7a55afd4655dec4d98c5c84aae564e30c088bd56457682afbae784d197a69a39203e7723446b64679f44aa88831010a76b7a1f82cfa2e38c432d84dd83aab54e5a458b364aa81ed5f0641873be56bf86ccdaddea0af31466bd387b677906b6772be48a7c0b3031ed7af075124438cfbfd831f3cfe1e4d3704b03888cdf50f46af4147a6dbf8b3a5fb8adb12aaaba9cd7b3768c4e7cfc2b1b5bd5f331c8c1eb241e0ed3c96e431cf7dc3b574ea04dbff23abeae26cdc324dbb9cab7449c437f3e94b54d2dcd9e126cfe5cbf4068c37ccfee2fa6e0a81dc75a9f8f227990c3533ea830c89c63d1806217742cb1cad508c93616da4cbb68b6a8c490cc8c228c85a26a4655537c5d29525ceb6af898bfc412853834e1e6d5e2024cb3f6cc9d353cecca305f74ea09e1cae0fe637bf548feec267f52449541e4ba9e68df18547ab45462e10f0fc45b7ed05cdd43a7d793c168b855d076dbb12577c7214d703cf8e45f1400fa760f76118af2e7fe69334a6b3144097c74ec87eb65d81ce1d6921ebb7b3596e47db4c837a109eb3975d87be811774b148961c0e77a1fd192c16133aa132ae6b75c9494e57467fe39538ca339a056debd1ba7bc34bd6d5f92c13ae3351ce26ad122f7660a062d30e252bd20afbdefefd01a3185b99599fe9aaef7e0ca01c2039797bbbec15f5af86e12776c13bbf40e49aecc1faf2ff91cc3af3bf708f1600d107f2e85672054462595fe83fb309b55d3bfa35db50a2da4e7717e13bd18a309e35a7cebb96568aa6953bb7a0f5e15fa4683772c60f49d7ad45a3896d863c84437fa1f55a459010313ba0cd7e299be1beeec45b6dc843e7d62995afb9b286378066a1411ff1f047b4d3c88584ba275f1db527f1e73d8d376b62b9a055f287c429c650f0412924a30a705bdddddc90c83f2b7a2cd08a75eb328c2e277d28cf0d9d3016133220f28a416d3d4d0c12d76723f10b0fbdbdfe462c3a4c3b6d3ef3c8fff017891cc4295d4da6a82f77649327e3de7c50a97233cdca0475e72fe66089630586e2f42e68978c13ccd0cf07ef377c482c68bf484039d0a78e1986d4393216f756ca787bd7472ba9356c2fffca52356a5212a29803cca2fcb27489329f959a296f31b9e6a15d5e29e9fd1cfb411e19e121348f9a5c9c92594cf2dcdde2c70230a49df191723e70d09fe7eb38a1d601f002db2e38b16a550e26866cfbbc8285531be2a4c298c41cb5e7efb7e57ac0d0adf5d70f55c4dc4c3f88798418c13c78042f00f82ec1d3573fb1103993d50d4da04f78e68c6cb383e8364fd1e8283eeec21a4f555d70ea02635a262aeff62eb90641a057b179add544a659e1c117676627d022df1403246bb42c0434fa178a83aee0e7b341a4c7a5ed9e2892f1bb6e38c3a84fa544ef82f0009e3d3b3a52a77ab28c03a10ca187ddbebb49985e8da7ea397839da786de3cd8952ebc3be50c485bc3a75ef4774dcac375737dba16a0dfee0f13af0c43e668ecdd891a56645b267adbac55141a23e6b98fa4c87ece6278369a1f95fc15b67559a069c665aeea3a7b195bd26f48dd2711d705e93004978b99ab332677950bf861c1059a52585c3566f71422747e5773b471f7b4e3a8bc296c6b0c06fb2a6021b3edbbc31429646c0337f95a866188f95f1dad7500ae0b8697acb4b17092e2a6f318bbb38475e396a3ff3f106c26ff738760cf4feee9119708d9f8711257520a154f19176fe181b367a23a14dbddf403f64855f9052d4ca4cc3b06d720d45aec7c0122cc5f1793c22b9cb24b538049182ec9f5d056963783ab3e63e230face1276166e054a65d955fd7e27c02a3fb46744cb400d3a3375b6c24afaba99287354551987373201688aa1cd349af2685efe50ac435f919df4b83ff1ff525ba01003e7436cbf391fa58ac56025020be0c07f9da6fb47cd93039e81b8634e825b297d3c46b339a1949ef7149a17e99465a21793acfaefa4d59d1d5d65b8020f5d24f86bc0b48f0a853b17360831c4391d18f9cf9a580bbc8176bb31f4f18db7967548403fb404476f071902df527e7c09298a95bc9c8dc1f59c4cf19ad5e6316bf25138827fe24dba0cec659294c2852f375e597d23af91036b4be138e7ec76208f63ef0d29db090da67a0bf865ce9528f968283796815e012a5d3e4b1aa008df7f750797a1058c782e700bb5d5ce0f73eb09a16bac8af921885aa46cf6e5ffab46bd69713222ee31fd66f08c592d5af13c0d181060f6c41bcde9b60577752ec57a12b30cdc225b2c33b7e3df034e3d4338fc934bb9cb75e88bcf7b6cb5717743b33de42d397000d16c2c821610f6338ac2ee4785801de2ef18b7fda824051480c83e9b32f7914c536e68e92631ab5a74b1b1bf30e0af353b0a2500f9dd059ec4a3dad22148e842ea8dae067a707ff73ead3132d8b894a04dcfdc2137ba4d9efe92bc255405a44e77b5956790cb94b37923dc49fbd8c0cdabcfb862d43274f02ea0df43fdb4a60e1e10dfb3ff66ac8a2927eb28420046dfb1f3c3a391310093fda9b8d8ee124d62ba71585cc0a6e009087d64b809ac572d2cbbae32feba0d2124908b1e2f1e8a932d0f964d6a1a4be109bc924a61e96eb325c967776f5d2df46731ce257c3cb8131f7e0968995d00aa3e7f00842242d9772e89cd13a9158bd08590422887d9d626dd8907babaa0bb06226f3723a1eb7f9b7162fc45766ba34197e5e27d8392f258b176e7b4a15cf972428539d00e8838acf0ab6f92bcb69def59f36a5d188d93298d7dc7d311ba65604ac900e9f295009d5d720fa1b4c499d4a55b0639f0e5fbf9959bc0ac8f6d753db1691c8c1f92f664786083a9e3e71acb49d000a63917045f5f952157b4862148058649770c55a43f87133bae7a31bc104ea006c8dd35d4730e897f8430912b0691c879a9dbb0dcee4418aa635a183808f0d195cd523e22818f5e28458171cce5cb36806d0617e089548c514de1ecd5828d44b374d980bb5c5560328ac3de2fa67757c5ca78548465f77708fdbd7c9feee6041b181490ce4b0ebae6a9dd04d39b47ca4c85f90f914e4e9f1ce4166ba8b8c9ccd216c145062eb011a523f126893d29e9e5ca838e058f24fa15c494c9955e52c0e479c75da2b5d1c1bdb406c9fab9f6ab330b2657890ce8260f092171403892e76b894377a5305385b05bf7aaf6ae42550e5e0b042c651280ad6ffc7b8b3b540c366de1e1f8d24f4cda8781f4e342134902fe22d3d5fd43c254b124c579aa0b12e0a36ab50a50711be68781c93dbfbb48e25c0c24c38a47ffead6c824c4762d82aa2bf941b3d327334f9df32dd9cfec11066d2b653b38be0df588f48b287e89a8d8e93123d79a55dbd9e40cb9bb8e49e2b8cee4ee345afac5a566f64ebb26c061e0fc14b99fd4fc0ad941531f66b28fd4bd09f7ea8c7a22d7aae2595ab6bbf287fc99e9cf7464a9ce2d94f2f1a8a2e9bad3b5bfacee1fdffe448f662c5562e02ffcb54eef4960e843837f3155c6568f5d53beef76040e39539fe82d9bc354026d3648f95dc9c52d7b84d9432ae57a3372c8e87fbee53597960041ec644f7e488aa968dba78625c75eb6dcefe1b5c49adb93e6f475b2fa9d0669373c9d354f0f76a1b7e870eeb1bdbf55ee37845bea25b1ab1b1519ef850755854acdb4812f31e75ada11c45c5abde433ede42988243b50acaa9dd7bcfc21243ba31f8ce768218459f720889b4da2203d9c156f2e1a127b660dbb73fd1433e68a88301f398ace7d8a8191daf489eda9d1571bff09b6b66c93cf5613019059bf7ffdd814952c428e0d532016ec3ce5a02f16c3cf6b20f051d43ffb84f403a112cd8c0f9664a68675a26d51ff20b02b357ff717631101a3c377c717a83f2d676bc21f0f62ba2a86e3c83980c8141bf41e39e1e0c83ca2be24cad4b86693a9e9efa3b5e3adf6b350821ced0d33814b3e63c0822a13cde7d128c7b7db7379a1a0e17968dd01bcbd785d8fff69624026fe6e3ecda67601477afac1f80c7969139884c74deed7a088306a5de75c887c6ab9b7dfecf976b45823ef07aff63ab85465980badba0b022d88c6f7ab29e26a6eea5fae3e12be512f1966c4a639b2debfde2a31d9a84d37b348a82f397b1512eb25ad961bf714cb79169e6b8e5862f180621257d89f160b252ea239af51ed5b9dadb9035f7636c92e8424c45f50d6438b7e202458f53a3731dcfe2a8c980c63954bee28925cfe0b748d01ad533037744313a54e1fd22f16ffe79e9c96473db8c15a094600422dd50e18dd44ecc3695ee406ccf4073c654ddc2164a1121bdf8b9bcbe3517cf2ddedc1e83845dd78adb630dc4353d75973124ae8e516113ffd329e90951c7e6d32fd05caf7a6ab609d4dd9b9e5dd2daa19a9a5e161465da5fd5b85935f56f267616c71a838ff530c964973b8023dddf3fa070ecb3f2ac90cbbcda1a9280d24d6f621d3f60edc64c7d153d7874a55d2e00fda9f6bda1edc34a297c67fa856c593087586610ade77bc05d1b0138b5d12350fe1f330a02da812bcd5fd114d7218445affaacd06d3184479b94a22200a92d1d846901c1fbb9f998b52145364ecaec527860d4e016f4b181ac5d5412454eeeb405635acabceb531816f709f8e4e78604eb5d4696159ce6401ac77ae1be49f7d638b1bb0cded39c702ca2e9f32b4d7fc50a575a79bf5d8356ca2ae7231c9d3d4daa4b85650bddd8057ccde55a8b1130be30dac7862479a060097870835904dbed8e7f00168fdb2b94769397c56f60e77c7941de12f6f3a065dcefb7c25e2899c4b571bc9040769f56d834f7a2a16e0be3cd8ef71440c52a4d6b38ebfdf7b2f839976c66e60cfa5cf888c599a470c7514f8b3efd7bbf88ddc873c65bbae3e7d7908a44d7d21a75b2aee06b4417efe38d1f8a71525dbbc0aa4ba7e1e919a0c85deba21177820a2b475193281bd0b0c6f943a6ed6d1e8a2dcb113d865d85b05ae30be32198a072b93e4f52cedaa78866420e1aec20f520c72352f0308237b0073595640562fbfef55913c860f503fea1dbaf1476dcacf3be915410f232a9abc4e383b5dbaeb0cd34b2c8482ffdd58dcc301278fb228e3ad714b9b88b3edd6f87a0740c4ad3725646520ded53479dc4d1fd4caf3055d3114d95cfecc32c6c010cdd5a44e25d93d58718fdaf781005b53ff78980d6d17e803a8fded05509341a53450d3faf802e2916e15321a810c58fb580f7bc1269ddd4bba0bdaf252f20bd9443259ca524b01ae3500e1e59faf6d31db0e9f704927c1fcfb6f0d988eff94af1f83bcdc86b8eccbab646df3634fc9d6f9e9cbb7500527c1639d4386635f1964ccfc058569d393ff64665eebae5695d05d592885928493d96bda77332cccec4e55957ec09d9c82bdcd89c0f6c753364a3784baff8d5ec05192cc8a4b956981fbc535741022d9d2a904a8d39166bea3869a1bffe8366530a622627ab2604beb0c680e6eb6baed17a647c13018d5dfb4d0c290d4dbe5b6b758f2857fa8c124b3f12392520d7cb635d146434bca000f7b92759256a1138dc3d5444281f659d166c3843f088664f38e2a3fb610b2b23eaa2ce3b237d9e4f3d5ca6b063573e16ae531430524b9c2d07f9ad858270b99e39a787e914233e61623db3b6435088ebfc267e614481b79b86b967467e12fcf58cf30e53ce4c73d6d28cbf6ddbde19d3bc76f76582b1e45261c4e1b493b59d0a784209c7d666dfeb7f99c209c7010daf208b8d859f8ccd2b03d831c2aa7f3699177089f73eb32fc2ed74ecd8a80ca8ae6aff2647850d529ecb1a12b66924024c62610c828932ee94727cd018b7ac76f073bdb842ebd2e0cf59a8dd263564aebf37b9b4049f8c13fc196ad25034d0b00c5d1c2c0d2d751b465f59a634bf97863bfd305ee298f88a42254ff5542acf175c5092d480533aabfd515efa44e8192675176a44edad4d7ec496a449086392cc285a3080652e65f41d108676eb617f33124643d4c8bd8e32f9516a3c472dbd7f69d4613ef97b4703fa7ae2603252af993132724b0eb54f3ca195c0cf7bc6aa741832cd068fc453ff8fdca9e5f1c7be91e06e9a76d13e1797fd8962f25b03383a8e7f8e56f2919b659ed1286beeb767ba91ff7680a91cef0730b0da59f1766c5e2e1544fae605eaebf8c244fba2a7ae6b7f5abf0122b485ecafb1c73acb0993adfc013e6e5aaa0de8f82d8fd7db9fbaaafd054ba828d891b6941254550fb81c57414373fcb6631e07f9f6938bad22a620a9127235ea08bbf7941ba971c332664f8d96603607ed024352e8494fc977c89e5818b97b8176ce39491634d3a48ddf27b3d91f76f388c817824da597ab60acb159b8fefc68465a2d3b4df64c2abf7665e9b13987053f3a6fa27a86be334e7d6f0fd418e8511c1b1e7e0d775cf9abd4593adc291ff37944657e98ddbf8412828c1ed1e74b05ce01b1d14709c2175d1a12ffbe5ff663587a014b8aa10d2f066d6af349189a549f618a8ca275bb4acac8d2549143333886da9bdf505352689cdc5ed0b31380dbe2bf45ea1dc93450fef5d7e3d4c84423915703adca5ffd00a92051b3fad7f47e6931b5eb12ae2ee8ef59eefbab3d3b1fb1365afd18e2748b525b363e2695993d65c28937e3ab6ceb4b7181f843c85a3acdfb566b061d3cecefbdfbeee4bba99dc5afa46982080086bca3b6a8ddd53c18ab7155c69682dc5b37634a2e75dcb5bb7b7e4bba3beb6f75845ba4f303c912a665426392cd1a64430882c31ff1645a78fb77acaaadb7912f7f183e9de1aafbb6d18a0cfee049498b781e487fd3074eac23de460791721c3731dff9fd7addc75c99d085e4d466c94e359c10b2660e407bc9c90c3f2224e067c738215907567f6fde15c59977ad0ac2c25d766b4efe8437ce92cda6730b1436023c97a9948a22bdfa79f0f8ed22d0f529dfa0e4ae94b9a901c0db4e2e1eada81a2ebe3b8d0cbf03a79446c3bc073fdbf3ea6504f6fff0364d78818c718cd42acba37459443bfdce4c2d2e2382e5efe9a6e6578f7535434332f2017c61117ba081fc8a373a016ad02ceabe45e3bfd1128ae370304408d4b0c279319671ac01a58f40caaf3b5e7db0dd5607204c261bd0b2e2d6d49f681e6ae3cc5dd25b4eeeb19000ae1ee877f1e0d7dab95baeeba4ed4a1d1b01a64694fa5954364d823eee475366628dbfad1a24212b52dc8832e68f10a9ff71d0bb85c7472df9efbdced8f73df2e9ff9356c756f25bd71f3485fe1bad01c30b5087d5613ec16073203768c3e9aeae501f28cf7d184859443a24adcd3fd690812ddf9c2fec5a92283b7c6a14da27c37535e4d6aa9b6b9ffe61251918ce77c91d834e075d576657393ae71ed66ab4728aa7915c09c90f0295acf456f736427911d5f7373dccb2da42f9dd8822f6ff8eecf676287f41e2da7c8ae8e2fb28847a89548cb7c2e2616d9e71c07835908a260527340b544bb22b99e9fe155435e53f4752e87d472cbe74238be0cea40ac83676c4f73cfb14786a8caa1dd6c0e5c5eab0ee9d21722b6295c0dfdb1ab51ea76ad2e4377eaa01c757b5e7bca37b104cb5692769509cad8f698a21bdf6fb2685255f16c008b4afacdc610411f6254d6be4f20fbf71c26f9f3897bdc22766f16bd760ada8171fd59f25885f01ec494cdcd76893535af011ad09571db76824670dd696a79c37691753a06e79880890cf49a8d82866d593f934acf249cd2c05b34fc541016ae52776e4302b0298a61fc3becb0e3b9e3f14642bacb6d192c74bff29db0bb1072ecef7f6465a50bf43d7672f67d43da5e858b03addd5bf52eeda836a767798e82d5cdb1b41b38f965c592e64cc66d4e5ddfe9074256483fc6163b612f624876e7cc4a189238aaf8e002eaa2932084c903b41c1907eb8500c73a53ba315cde787949eeddec12f813052f36d4f2e41569ef262dc7e892c279d785e8773b5613f0e058ffd214ed5ad3c0bd3617e3c561355871c0c6cbc69f317820edf94c4193d173e93317c3c95cb609a34ea96c6ca430b8bd1082b3aff5839eb9a56d98b91000a893c5434affeade227ebcf21968beb816c68527fddb8aaff5d0ca9708f9402ac1d9986c455aa98460006ae533754f9270205a4a489f9688dcfade43311585e68fcfde99122a1ebbdec8b9023727e3810d3a4b543ce55a55e4abe511cb60690cb6de466b2a72364c34b914678d4c078dd36acc597bf1c244ec5d679956a0cb10e5b1887cd30310c34fec8ea6cc92b87bfc6c9d28481da4248e6d7fb16774f6ccda331c978acebf2c07d92381b83e3f0858544dca0be794ba0720ac1e9d5ac24bf29e8458fbc8f495a8ae18be3d6b491d7c5191715ac1ddeef3349a84ca620ed42f1d6c72099b6499ff05f324a545670f4ca84c4674307d49ef9574dda3bfb6160d6e21a24d7758bf019aad421f501cead4744dcaf96709c412f0914131d81ef63d6b09de4b5016653f9de302805ab25c6cd59013ea8928f206aa4611b651bd808ac1b7c9ddca543b64c34014067f637693b7ba8658aa8b693e7bda94a5896a49240589095137e7790a00a3e8d76ca263e7e4a3c60f6e95e3897f44be0d658af4b2d50e527826406b4eaa08c4b88df061bb7d44c3b486df74c0fe4051952180ee2b06e6cda9ff69dd4297d24ef3fb1a8ab688d5bda5821faff82a785562ae5e4b84fd962f21a4408c2876b4c613688625a0938f6156eaad633f42592be888a56fff4ad46e2c32cc1e1ce6da1678133793c76852d702968cd68a2dc9389888740ac38e5ab256bfe9ab68840665faa27e226a1a51f453212fdbaf65e262d2625f2071de405f65a8993000e248407dff06094e43b9a029f0226484aefda80c749696d6876de96d9f716eb7075a6738b2432599e1325d5a2a81624ef4d3a08e4c0086d8ddc4253146841536d770326cb83ad7dbf6a502c16d21f7d57f2b7ec016221f7c27fe79e8227ef2364ac2a3b5aa5a2f1de4de9afaed6595e11d518217caec302094f9c4631744bb0c8420569e35fde9339e12948de79d1b6d15d8813be1e7ec0b194625e5e7e597371b4c4bb74344cf2e331694488ad49716ad8caa6bd72fffff31ba337410dcd8587dd321909ab10e045e7e03f3ce3e2d2f22fac1b16da1fc38997dc2b6f161b34fc37d4f7f83a303deb67b76275fa8af9686bf5a88b996ff3160ec86eb437c60a2b2d02a8d46d96431aa2e17747a06ca283b047193ebf1a100e917cd38829517668ade561f341c54c98108233fea8dfc949a3474596927f05c935d3ae3735d7ed44aea2f9a74129b877939cbb94b7812828023a14b8198f03cc7fe8212bce7f246841d0e5bd336d6e5ffec824a46c0a6c4013e8ed40a23600f58a674a000b58eb468bcb0f4707b20800fa979aa0deb3c9ce76c5f1f7a0c83d2fe54849f72e56b8ae0f778bf377aba7fff8d2aaea419fc39238c29588e797c985973ea89313b446ae866ea3e587e878323995a33d6b6e37eb94a196fcc98dddabea8229efa488bc2a62d9e2593e3c3d47eecc741b92335264d66dde70d59739b7cceaf560f93d4b923ae7374e9ff7d1f248da9dec7aea2558de30c36e4a288612f14b1e4bd1376904d486ad2b2de36af21c15d0950ce22c6fd9b31100754bed1beb70e65fe2ee7075e548083cd46dc66115d553cb29d7716c7c9d434c76cf9649609c62ec0da9d1c17a3f8b9694845c825df7490395c9776764be88e06066c158efaebc86d61a5f1c4725baf80ae147b1cbab7266571786aa4edf6111cf92cf957376a59108d848cd6a3ca432a21d3b81a1a1a7d8e32781906d173b384c0676fc4b1c307788666d9cbe19e13c8c264f4525b79d6241764d04f3415fc45f1ce9afeb191155cd3e838d566ddc5f35ad2dc07719053139d7c99baddb005624afa258feca5653debc5db64c9356e91beeb2226e8dd260d78ff42918b3d91cec461cb6f84c61c34196438e4eb2317c280042687cf335ee2cec931fd1fbe84a8d6896c41c7904837dc09020a25b87e9a7971feede765b82c7f67cdb09fefbde6b14ed6f72b0bdaf63a99304fecf57bca5adc5c1b24cdf9fd94e6d4983c555cb2b7fd0aa7620171acf38edea574c325f4c5040a5515c23b82beb0a3aaa0461f07049935b8f7473acf256ab4cb3630738485636224159fc4e1a6f3abdd7c5d2c0760fb882648160bf600dedcd4bc6cce18873e5e59697dd060cd1d8ebdcae7ad1b6980e17d1030efba2b84d84e2d59570b5552ba9cbfc4a3f45f2f6ab5c192264419317c83b292dbe61823ef342881f976336008a8a814714b33b37564048b545a737fd7452767c7facfa96f10a70af956399e5bd1e6b6e4c835478a11f6977d1d521d082324ddc638769dd79a4537ecda9797e27563d5789bcd6cde04d08ee341457d015bb197ffedbd6e71c499a33ad5c23490bc4b24c125ba44cea3c4f644c50d6c64d6a68fb394bb9747ab74e51c7f46d81f334fb09191bb49fac597b02391768a0fa74bd8419e2d6ff60a6595f64f4884b7a69c971fb8527fb7c497ffbddd05c848999bca2e78d424e5460ebceb1d5b65e70085c34e9c105be7537b6c28d11d90e5911064611273461bb261d6f6ac199595d6ebdd2905cc5b9cf697e0c4ed5e48f07ad32818751ce93036fddbae8d9334f58aa5983e22f53dd7ef0b9c88479c3ba31df90b1a874824db3316737cc4cad1eee9f6311bbfe3f23d2ff929fbaaa2f8a7d68f47307cec57657e6097e63cb7f12f504ccb0f465e7604e334ea676c21fac0b44ec5a45aef6d9813ddbd26dff0304de8a48ec8aea784f599285cdb25e827e7f77aef2468bbf724ecd32234717b1480cd5850696461938b30a5ea95d2ae3e57ff8ddd0f3619bafe19de28a737bc52e51c2647db5c266ea68d1f53e132b7c93ee837532c1c87a386a41aee1f6c2c169d8af4e9ae86d93ceb4aeac410726367228a99b8dc783530b1c38d77e7981d0394f307e58f4bcfb8c3e760f465ec76d179a2688d77dc9f7ffaedcaa6383d9344aa018b8f9a85d25f3b7146b7593513cca630b0de455ac2e9da71d6fcf5598117bc0658d6138a4e9fe63ce0ab108416b2e05d088e06442b0bacbd8cb73290d518dc19979ddbd0a06b7adb30730e400c46b754ac660b132af46f68b193a1990d9c581344e121a7c88f53e46a96f9fa48ec04f844556f4f00c8f00c3ddb47ce9e026f284a3e9a403c8cf8c1b835a49529f81bc7ac7f7ded56d85fef021447821d60cbd3e9a57ad4e5f5d6d76189118b58101a4bf3a44926b9ccfa6cbb29e1b33bb000513a344a0f5c72815e3ad96d99e1974479c58cfa4fe059c21aa5b7a963222bd658569f3cd1538bfc3d2b6eb2481d2b6de495b54b56657357884cb577944249ecc99b27901366568b4640ee351d152c5698d9c75d823427736d4e5d7c3611d6386d6fc2aafb3ac3b56e9078ec1a98a4a8e4ebd44355f36867ab1f9120ccb50e3c0dc07cc4015455d1a23bea165ac8b174d5af43229e0bd9f462320f09fb1385932b268b1376ecf228a78b7ea2d06a6f8dec17277e3ce380dc001650e7caf9500361f3caabf17fcc7ff2a13771350aed611ba0804947587f085950c30682112fa605fc2bf00f3807e1b938f3d72129b51f52af53fd8144b5b0f43d57ff98ac9f18de436ddc956f05fc9376c406c8a1f97f54d9f06820be933e010f14bd2924b8185cb71094044ebfafc6230b150b238a64e1b26ea9b7765ad19287a9060b74845f243c003d19cdb34bc8f9bca34b8b68a76c5c697ec90efde2652008b43898626fa8b03f063c735b3649ed1a4598ef9c67c5fcd52381843f205e188deb0c970e185441f9c7b9ec2c583104472a4155b62f870b99bfbe7f8b4c0bd9ffc81ffa2df562ff090752ff69edc758ed6333d96423b643a8b11d1e0ceae055d91cea29b0b2aef4919781ba5956d29b24687e5017cd3ec4b3cb2036356c192736c5a96fa3735c5878a3545712037d27f5360510f2bd023bf28643e618976998ce7570b1d5650b1c35ae3f004e1c557adf6ba7e23cad350cbd25c99f3f51d1aa5fe6e890d32b6da87d8e2ccd7319cace0c608d0965cf450de4f6094557d8910b4afde72b559748d686efc0553bad559a03e5bb3d758a86601310c214ef8c76fbf6228e2a0ce9c6b38ad949a3b1a677edf774b2786c2ba436d6cef3ad2d53abda3fd06798d095a3903afc3d4c6b80bb7f3990b86d830823e6902bc157fbdd57429c7b311299d50a10f79711dbaa1b53b5745ba725e6f5c0deacfe4eb4fc67f422e9f60869d0f36689aad9717124109a4f6a23958c3ea69f38c5b0ed52475c371c1b0de402d1bbefc7bcca416947f936dec6423dfea4f06186fe6f4f38582c1dc705cc51f3802fb8dafea19067016360960b9b23e0f1af5c0dde63c19a7a2a47db6b427f68f648e2106d58b4a1dca927fbafe4a9c7c1d53b2e616baefcfaa4fe37da645ee3bec952a831250068bedac057dc173bdf9dec997d4238452788e1b57f0e416be9c5bda6612547d60c4e60137630cc79581c04753ef7da302a8b3f2e6d98b5519a4925d021cbfef1bc9f0725de441bd03000369ba382f9e5889fdf7cfac3dc3794651063173d0f6e2ad415fc71e7b93f5387b9d69b6098739479f69dd562e4ae1a715dec2b15b979f2a2afa5ca2cb7c82515021692c385f2421d8229f338ca472ef50d0fb9f733f896cebc6d0db9069d9a6b5b7ea950b960dcbe4692d3c37006e9866e91d347a44e6cb32a375fdd6acbf4789e51704b1d00129be005b41e556283de8d4150b5abafe58ad589e1773c81f62ce588a7767361be3dc435ee4506fcf264ca52f113458463f92301aa4907503616be9c6771bb36d1d289f053efd50f36766359fbcd043199667e39051bb5ab08bbea4c1cc9a9c35862131727df17b45eb79b1d01f2c6936214416399fba8c90e8dd08d2006b3ef0ca93c4245970f28f2930acae89f27f46905538825e6b6640029311dc92c37cdedf289653d5711eeba53da2ae05225496f711979086d1ad5a1731c6b890c93e156e7515fc9024eafb5b33a40a8726b6f2c36fd29b18207d513c6062ae4d23529ce06d3e3de3d41ce2892126dde7c8ef28319e5d2755a83f5155ae690e804683484417f1d4cf548ff080e3fc2289cda4f4a1a317b673b21eb6e045389b555b3fad31ce180a69b31789646eecb4ac8603caf77ee5079621207658b2b8561a93749fb1fc305bb40b46b71cdef2b43906c05ebdc6252b92a991296ea89720e1121d3e2598f4ff7bc1d3980768b569753ffcf9728c3a1b9e001863b0af340bd22665742c51b6e7d438274bfca0cb91ae979a1987f5e65a3b47fc166ba79787e8a8c9121d5578b689473ad580f613daee04556f43464a631bb14f7c720c6690e01b17d34b01eec6778c10270c1a30717affe00769a74ea93adf47ba8b620e2ca254904fd84544a71681265341a474eaa143f69ee27c4814a56a6236e524ba0d1de4afc6ceb2c89f935bae62fc0914ccb57dee2fff9737f62421789611f54cfdd0f562afeaa554e7eb89abc761e9bc2f76d0cf71996ccbb57ecf288489b6bafe5b4527013dae322255d8035fe0c2a8e5ab8cea09f1565d933642d42b80877d7952db97880267ec32224d49fe2b5d61bd1363ced6893a7680c2625bf3a50980ee3fd50fcb26b2f16ea9bb9a38da5056c07a2eb16473256cd23011472e6ff7575ec9c82c9fd9d835e631611f22cc3ae33dc83c93fe4c00317ef11549c2fd7e9de2d10fe5ce5d07d2b3950382c20e9935c2b2e3a7ef240d8e6f30ffbd2574b3a52d8bb786647c2f96372fefe8354e83a18f7526c43f55caf6da25b153daa5b2c9693579c4339c908de4bc3e890e39869d2420f74104e2c20ddf8386d09da2897601ba463adb81b1bcbb4cd621887786f5441b382ae6d2a493a3799bcdf86d356108426eb16a4adfdbe638a708a71f4f0c16ad4cbbbcdad69a719df40190260ecdd822c226833b1a5e48d79d5a700a039410485f3b992cae0e57703c97a4ec75c730c24c8618971f30e1c90e3b73eded91cc7fd3c1a10874c7111e031d322d9aba7642d4223a2825c37ca12db28ce76eea4095019635c8e0c45c8d9d7f318e204c413f218632b1d56cb450c5086a22e88a243a60bd8388992104f681f65a4680ccded1bbedb314572e769eefd0b124907625e306ef485ffb5d17f9c86176dafe8b4bb6a6c0771403a19c425025cb62a4fc1d073b7684c88442482ff6275c8d141a8f45def2c20fb0e9dcb746191f38353cacece27993d15911eefe446c55e1b46c29c7ed998b6ad10f66a5eb112baa6c806101e525f0ef83af20e9d8383fe829f20dc7c291c1f411413f39e5ed435e5c8272957c16b99af53f3db8dd05e009efa9ab6dba05d1c5e82a470a80c94703044339def64b6f7c440f8e68e5df0abc03d3f602b04cf9f0a3f06f2f02b3415b8edb3f7949ea4c2bd603a28b82dab28e49aedfcc5ddeb51381173a5ceaabdc5963eaa73afd7cf9b243e2c05f598edb90d5753980f5bda7c9029f90b131125126d52036e3e7b9ae87b0c7396c71f863c5360282fbbaae1a3249dce7b4d95ac6e4dc23df29814787af4213d9081733e4c6381e5b5fe693ec6fc949bb1b5718f0ca91178a5259e3a0e52b2b7938097901243ad56388cd2291796d38f1e23203da24338e3f433807a9d95cb7f7c2bcdb0c05eb11429bd7e3fd30a2408b69c8a521f0c406beb3cdb931ae8f87b9e7858154ac17285a85e637b0669ec9dbeb79ed685833af4e9fb2ff33270fdcbfd06c030dbe676bdb201598a432c74c5e501ad70fac290d36fef907da1db81d4d09a38c6381f5816b6b7d997a38cd820f87b5c37e8bafbde7f6655f0328f58b3b23ff94acb120f3707041c0d6e5efc3781713c17508a76fd8c05410eb099381d651371c3f143e6e1097d67d5f02104f25863a158427381b67c4a8cca02ee90140c6d089aab9e2d4bd4e2e25061e49ef4f53437e46962d4d66404b0439427b1b293b9ec1edd34e7b35d209c46466870e3e7e291795e97406c27b60632b921cc91ab377811cda9db31255c2822f28dde09f9e6ba5dd90b8fba73ac602dce46240dc639305805d0f71e94f4861a6bea941109d50a8e34a5d874de60646db06a4838f0cdd8ad3e34b094f4e907a4a5338727401e2e7ca52763f72ba978318ed0d7f7eeb79e3dae560c58a5bf4416b01fb315f6f91d469a6d709a254ea608ead30c88b464a8da033c8b28c89cc708ae86d9a9750bcbc38bdc5172e4fe0fac9b5c1d527cbfb2211935ca5e651d2ee7bb092243a067ec52591294a37b894683e8c32f723c2ffa89bcb5b9b646ceda718e0e3010db8c69b95e591d4d197440352e016aa061a052ce56a62609c457461c6f690582e420318e3dd52cd61a935a2b5cff0d23773b161dff8366789c8da565338fe96c7a24b10f510bee4e047210a5f3122e92c93d36916796f051e8297d43695df65e507640382746d36d9ad98d6a99f04dec075e7cbdc977afb17acc7b71c00f7fef3e08f8ab3d4a8973d12942ecb94f0fea328181ea6eaa68956443d242e0e6c75c68eb0114460a6577fcb0bce146dc06d61dfcf33f4228104fce3c5727ee8765f1bb94eba53c03e42b7f02b0f50f2e34eb48d51c0202ff2b5494a87c9aa2803996804644432250c5acf036ecb8b769251ea29178dfcd5d684aacce1475c02da26e9047e2b8ecb5b75df1ea197372e51b1d4cf823d6b62d665f82ad5ecfb79fd1eba80a18113f9639d124fdf526735639b05e47fd523a5e7c2be5cf989d49740baa101399575ba920e591e9bcc00b2c02f603dc4633175eec082ecc5f29adba09da9aeb28e0a47a110defd6e40358eece81949d95d01228cf08072bd98facf9c1ff50a683cc6b70439f8ed2edc4abec5401f2132fc67bd70604560e72ed81614853e0840ea0041304f778bd367d6295af3ceba7783774473d08f5e97af1470ab637f7251711b81628256f33699d086d926b1e87ec210ab031d9d36c18dd1e2c4aae32dcd1e8fe6c6133154fdca0ad09c4927a9265b2f3ed8a8124e9631eb37fa2d333fc193a912749e78141586ef8e060ca338c9f8b15822ef0d41266b2285e7cbd369a84bef11b9489668f78994dc03c420534d407978e226848522bb6180057c9ecf78a3f186f102871a607b8c59658afb3c155a9ad0f0b6551fe4f0e8bcc50eba640eb68658c0a33d438a766bb89aff29051f87ddd17040b69fe352e5481757eaee804e1ffc102adda33e4a886bbbd0f71e639226a742c41e57e8000fd7f27a9ffaea625dc05d50d62d0fa44f666b6174161c013baacfb1896f795b4918eb1bbe740b356f283608d6ddb754c74ceb51f8afba5b5c2621c05adba581fbf6c695b923fd7b3fb135281e3227fa32708383a1783152dae59e2b488aac0f9ab78a1fde00156a9043dc3a20b2d8d818a549fb0f45f422fc0241221f500a22e0a272079585c606aacfcad20e27f6ed66670e9604847d3010679cb49c4c4beaf9db647ed6dcc4629b14f6faa58588d64f99fcd1fa1b47e837565af1e2d6edae0864584a04483978c169a9bdc420c94edd995781ea4ece087a39657ab039bc9110a86c64803e6de6654bae1d2f695f5fb5529aab94ab76e33f25f9cc6b8c565e4387f4081ebcce7debc4b4ea6dbf422142ba1fc5d113522ebde46699a6b3d7e194b456f88f16d38095f3d0144df86eccda0974ea82480c048eca9c6702275ab3b8a9a4b6866798b81b790186eab7ddc19b428d3c1c9e5383aec236aa2c1fc42a9d80e3b672b2d13ddc43d7570027220732143c3f55d823ff67b6135afea20d95432123ee5cb2ea96072be71504dbe6e2f179b59784a95f40d44885b153252631e44b09474882c0cc30e7fb6fd4adfd23853cf0bd060758a9685de3741afcf8d2f7b5714bac6f9f3e9fb2344606775fc7968ec451506bb93b681d0dc8ed161f48b16e7714d0d192691daf4fe21984ac724323e42ec4fd8a62ded96f45f5bba97e8361826574ddc34c13d53f82dd9bde6a0f0b6541769987d5afb063cf04dff907a11bdb6c9df93499aeb8dbb1e98c83549ba2a8bca0851a751eb322c57b79d381fde888ef40ee46947c8000cbb0482e49fc619674a068f58d0889184aa8b929043c213335a7a533da599f4b0578be398dba884f652f637a21947f22e22d2e067ad40e4b999ce99ce4e1a72b35c6088772dffa54ef37b22a52ce496f5df1ee255376d6de4f87741eabfd378ad51ab27b1c5e769ff1e8f1fe14d24417ba1ab3a03788b5fdc292f9e64961d9cd68bbda3bda9ed02c53878a713945c147aea0fd073f4b5aa7fe126d7186b6a6cffe1af7dee68c4e36ac1d8072ba08b8f764f2310cf16ac47b5da0fa32f939ce84729847cc0ddbc50d24aa0c7ebfef1e8a0bab93bb49814ffe79c6b3b982a8e40d9e521289466ac64274e71399b7663cebe1201ae1a0b2037fa30cec942da3cb807f5a07e5fb0c765fd331c1147d431e72231257be59260beb610faff562867b6831e9e1674c0b27cf470706fd0a4c0f3e50ebf42e02e6f57650b92cc5da852bee06e34a2f4ec3ccc23f9d93ad5a31d21ad93838d17fabb06a4643540371c5689c1da70c9bd7b20b2e6c9df6844c0243d0c4c9b32e566d5d759f337abf2157789f183529f37696b9ebabfd1621276a7c2b9267a1ffac249f0f904f680b612e02a28b3cd67775a2e558b054a6fe0c501b6c63888a400e8123524443aeeb0fc21ae8db17ea6851466615d19cc97cfe01b4104e3924d3fb0765fc029c15133fb7749f4dcc4461fc340d4b5c1f488b81ceb23e724b406e841eadd7d4e645ead5b66d50a44f743afc749f8030aba8d607d2e3470479f03e24acfe571b393f465c276cef1ddfd98803bf276fddb88f1bb0a51c28f26b5714e2cdcc2b3d03ea5466d3cf8986ffbc331f68c1da9789b932d56e0a87f8b713475dfea1f1047e74930fc5bf2fc21395838375ee95c716210bb3dfd1649198b07a728ca5c686ee564e9a995991119b80a9655797738ea229aba44ca1b518b6fe993741b012d519b517c0e7c11e9162edc26fe776ad18ee4ca0d1de7c524bc8ce85640143afd8546fc2bd685796fae6f6a7e7d9a11284258ea4cf280bf75fb0651d4dfdcd93a760a72bf6597d8422a649be804fcfd8f58cc2bfd17d7ab170865762d73053aca25fa521cbb8f6f4062a17edb53856f5d981b940ed474fbe030b21ab6a0a2417bf6f7e1e14850d9223855877aa5d97b4e18d3809190ccfc9063da4fb7b7f619cd0076f18e69a4360a2674e51076810251a4db54c7650101aca637f2a8ee83fdb41e8354f3a96b06f5c5fd0dc4223ba71421500edce2709f5781b84edb1050799bb058efb5e3c5276da40af39da062209f17795013a132ed16b0d585c0485d412cb0be0a89d96f6296cce48dbd27778efa5e76253126712bcc46cdcc04d29f8406adedef925ced785f1d54c4f6d9dd04cf6e8c8d6fe0ff9db1c773e28c8f31b151d01049547bd88a9a61a62fe4f63688849ddb6ef2a0827fe382579c799309ae9061a5afe137a8716aab833216167bae93191ee625e88b02168ac1ec2beb8159646b6229dba2a3661d05cffb398abf5d3d85499a9401851b9707cd032aeb7d0024b2e0bb44027743e19ee6e02e08dfdf9952a1179ed09b39ba42040d0edc68cc4334bb13feeed5a36f51ac4fa1009489123a7f3aa590fc7ffc3955d03ebe0e63f7bf6827e0bb79d891dc2d09ada94706d9942ca40f2b6c4ba39a61812a516a026715fbe548790aa2a181cb874db868899143a54733dc48ff9e82bee85e9e3087c1b8de7a4e12efc8a47330b8d0336e08f8f3479c480ec0500afd1293b84ce987c6c5f21c40a5daff18b8d0f1b2489618175b2d00b41d7ee141ab46f6020138c385528c09b22b555e572306d7a5f19377a0644b880a6fd26239156aa45bf5fea5146beeaaefb288b3db4f57dc96e2acf944cfdf257a88a9bce58622cdc2657297bfa91bd5589a43d470178a5a72f29a2b90bd3788a0193c1c143007f342cfe3c0c6094c9531937b007263ff4aca1a9c669148aa2054a77e4113bbdfcb473639a3b9de160aedf592a19cfc31f9054fb8db0379135b088e47d1cc4513224c8fe2c4fc3a1f837b3ac3f4bff786689db18d969c5d938220a1216228a032625f3dac291483ca3dde0b29e82571da6d14a4d52c90c25efa569f215147873d9e285f9a12df6b9a45cf39d1140d5c42e8a1a0dcf07d8ce23ecd86df36c0aa720ff61dc4ba583557472d4c950c519f786d88fc0f702ed8abc2b0fc6f73712ae886f5fbcba1f4358295dde4a8fc9e62f02c23b172567374386f6b28075c09ccd134ce4c2833e45686fe604616241aadd344965a694c9b8c1c8563a883d9e86aa3163c7dbdc930f30c901d79263e077d4d8e94aa07c2246d967cff1adef2339a8e8b6d0dde534a2fc25496070e6f3fc4259425cbf8ce88ca8eadca89608701ecd63939641e0c451e3d08847dc98abe3770361f3f12083e88f8ddceca56c030ebb31afcb24c6f9f914102ea675e64436c0c1072727658072305b5bb03e98301965d692462401f0580f1850fb9f16ccb8caa7e7cdf66069d7f720346af59cda1924d52f8dcbb37853312470c855a9eac8260da90d5e9525e5516b3f4fff08bed11b9964d21fe14b213e51da6e9ee910b187eb4b7c48207a73091652b9f3558b43acebaf480ad3fbb4f2dc007ac2729a6f0ab791717862ebf205f53cf9e682f369d0e6493b78f3eff6a8c2ebece7d742b192573b62b30227af5654a27db3f092c18aa02bfe851c1b5a1d32a227681f62b42b96056abd32a66c3271e8125cf45831af9af897deed695d6c382d49b899dede01288a184166e3af06830b564c6e5a75e40ac8d11d008f0255a20b929099d293f4dead01c073fde2dbfff7d2d04471dd75bb91fdbea2bf11ff4de7e0b6f8437c55c5912419d78985201718a59ceafc4e72d3cc2c9753af407259fafd9061de94bd6d963602335713f7da3d54bcb19ccd44d203b840168cb411896ab35bd46653a16dcf5e641c1710986354cf8067be6b194090b941a45160f76d024f656d99753c095dd6006e1e35a31cb2dd28fd8d850917bef44928e6f1ae7e5808ce135b89d417d75e56c857b0dfd6bc6e38c370a8dd61c74094f748ac4b99cdb7e48a8c58881bf9fb472f4d5756881b08525ae16ea7f0e8b90a8918b4955af351f3ffc5c0dac83de1d17b0038b38aefba9a73566ede26fa334a7675dfafbf184df04716680c88b68389c68e5de41a27db6d042c609e766328eb0a156ce832dbdc9aa3e01d57e411d972fc6af017bfe682145bb97bd30964ce64e2de0e19ea4d940098a464917e7ae367816aa0922cad502633d8e86b64344b72d94f4d7a057ef02955896c191d413873153ea9bdbbe6c5050654bb514984779e32ec15f1dcd1ad5a01b0fe3f8983de3030b9a59c6be2353276e3035bc70fcc96613db2bab2819c16907028b67ea7b3470f132572d70cb45f2b4cfdd13d73da8fa9e09528ef02000739ba9c5310bdaefcf04b832c83c041f866865f1c8212eb74d791e05af3cf1f86286e7300d317e06873d8adbddfbf6f95f710324a283d9cc38e2baa2037c817337df4cadd86368f32c9db2fb9bface7e097c3bad70e73fc2e5b2483b261f6a5531bd4d9fef1447883198e00e8dbe54d87bc21ade46d515bbbc53f26a3695ab7615a66a5981e176fcca29a40a00b57ef1ecddf90fc438787af8eaa1ee0a56e933ead4be4380fed7054094886394f12a4d01b389e5d31fc74e1fdadca657c54e52dfdad81453e39250f24196dec5af749188cf34dc688bd9cebe7d3d229927da6caddcd65ceaf343712e2331c98d4320f25ace57a55310b1527b3d3d94949bb73c64d01005791545f407186f20e60175a7c7396942bbb4fdc499058d333148813720af3b89e4772ae2f001a3813c31e7d31f90f181dcdd683df3cf6fe67cdb387e385737a9028370e915b39ee32505b02e3aa578f46d66424934a3965a8f7aaa19977631fbedb2366e54d654ebf0c94e9f7890a97f6c3064c06fdf90cfc13c253e5bada549b8b8916a089286a80dad51a83284d8ffd9dde3eb5c65062c4fc822314a37616ba2d475014e98b98ae25bb47a2de26e7774a748058e330c7dc86e40e2fefe520475b0f2c91e27782a1be287f7b7b316108a9e98c58fd678e40bf375b954edf8b368ded3f6e1b0f98d0174134751a2080d1073d541a3d1b9ad08b45c2256fd56d7816759545da37e1ee5b1753edc1e4effc97fb85e915f7574d6eb34d1be2c1e332063b858420bca243b715f818fe37688888cca5fb0c9fb74b906f8319e0e7d49e76faffc39768112292335e9cb7c279b72abd6c8d1658c5df1fe59570438ead9e273a93cf99e9dafcffaaf5c1a0a45714b80e2c5b9f64080df577070eb6c7da344e86ae5ecc7a315d3f88a7e513ffa3b57b406e5d5d7902724b23f420ba4d47fd2ca265e7fa296b80417855b27098f211d57427e2e374c005d59c74e0202910b894ab6a4f454fe6549cd4bdae8f79f1449ee15e88c01b498334c22f689b98c9d9adab8c59e68c482aae30f803a15973566089c49f5f3d5d68f14408b8afd46801cd213f5856baa7f7373dbfd8453e606cd6311133f86ea9cec73d0ad53202b001961cae216aa51b10117ac32ef5fc9c443016a5d75ec0c193ce38aaaa3f67d7016e59c6feb0bc8b8aae64bdb84a84bd3815e2d4f733d6c1a65d6c1fd874f9104c7c15d7c85090e7a057710bf7d54b7126a322ad2a47282a3d5c997d88c5b2deddb0a01fdbecb60546ccd2abcd070f74db1223e7827a126a1f0499bfe9a57c719e95e7ebe22b508091955c7f84c65128c26b271517f1d4791620c91614713123aa132b2e9c906f5cb2355e6281ce7d413e1749b1cde94a5bd306cef02ae12810bed599c6b45f6ea4bf25dba2a7671c8e2f5230ac5f016454375025f78dd02e620381be479cf6ec87cf1da6f23c497d08b0cb9c77af843a63d48aaedfe95f57e8157ba4ddce76cd55541ed33ae8f3317399716a07af8492f0861a5314ff52af5ecc036f9eb1e20c2a15334536f013968bdbaed34b7d516edb7df45738d0c73356b2d8c096aba2cff6207916fc750d7782499d7cdf57b25b0404f861458a1d749db7d5500b945653e2ac5310d73212985a760dd31bee117bee48fa9cd17bc51aa8f17e6141358d12f3ba2f49bb0444382f4598797b82c82b3afb4b4e1b35cc8a9517058828bcbad8110a87b0a23445176bb76a21f845477478d57ef2739b85da99dc81f2db75df0fe403dd9bcb4463b90f7632a5e92c9efb9d0c1da0ab8654c8204f83818c39e74d600be8cd0f790c487ca1c87b0280834ee2bcde0d69bad94bc8845cbd789e4abcd7eafe5bb14f1bb759d7b8956d7ad8a555cf07e10da91e57486a63a4e772a33ce47a705da582d52cab6ee307c8f5ff4db8f598161adcfc11fa4b37a31eac77e6cb208bb9f58669dd57dcc0ca3eb8a48d15178f34286ff4e7981d599ca2752ff6a1d25682ab58cec8088465a723cf95855549a45dd3d0045c62ebdeb35e8c8f888cef86d828d9bfdf0ef5b298f8652064b1c4e9616b5c9b6e596d4b742082d8093d3d8f351c50d384d5cf0107508cd7ede88b1baa18b87bd4b7c1551df5ea55681e3b4c7d314f192f2f523550e6cd5b833e8f0db2b329a47d1e14afdc4fa3d8f48a8205fd89807ba21e826a7ef1092eab5e1708581fde7f197a26ef6e01ac2841266f3fb661d239f929d28016b7b17b3b3d9bae08a704f1df0f966d60e19874e2e1b1a0995ba8303fdae9b9d80a949d13c8ea113b8f6a95974d426c6562db0a9986d6fa6af1d9bc01d3156d5430b63cc7e9147b797a880b965790515df103a41c21392f8a628d43561f307f9d5f6049d4d3c65449bed4bc15b07a4b3e3c95efbbd6978f626d2acabdd3ff10bba7972d15fa54d2cefaecc1f155d249f3f190052b1346b175da314f83ff8dc2f0f6a617bf5ba82d5730bb9b4264cdce3e11d685297f73cd26aedd419343fbd1ec01de28e1e88d86a360c7ff03ccb9e3c1f117e4f00e398284757b7dced94a3ce11cef2cf640be3b086f31ecf0214a947be88811305719ed0e25ad946415bcf1deef39d59cbb1fed36faaab1ced1da68017c5c8d0818b368652ebb180370e0ee6b92626ac852881b6868a534db83e19e6b8505eec0b5ae058a856649216f17f7585cb58dbf5b844d3dae8f8baa560519004b6f8ba52b820e6e273535bc927511c337a1ae3b20c8d8e49186b5502706d258835734d8ed854ef7a9817d1b47ffb4a3f60ad02b7204797338488fe0a36807addd29697a5835504e71daba470e8a8c48b2ce07e2db16a5c98979573fa66b689f6c62be3b9e002ba59210657e83a672047b8b366e0fa1c71920035ca9d03c6bdc42c4b0376c9452eaeaebdb1114636e39e65ffd9924dd2a8fd97ff1439023a79fd0911f8942a44c8c49495784cd3279a96646e13615ca149c7bfced09b57d24edfc1a71e6d0de7e2942171bf8e667a89869449d0ae4289275a668eb5813c35bc0c35af4c784a34943adf0a5f44d61bcf454b2aaec14d1ecb8740fac8b497df2aa1eb34d708801d1783b2564450989aab851652d7449b5116bae0a171a485a12de81e0c43f7cb910acfa35985d7779eaecd8b603cb500ddd67960d174406f1b246fef59dfa86d05acb68f5c72d5165216290668cda4c633bba6de952366981044616e7e4024a24198e408de8339adf2b6bfe338ffd085322034f4fb06758c306d322e3ea05c98ff9ef76b8f4fb0873527989f5e5a4941696a8c785fda7f98a9eb031a27bc99eb23edf495fb375224889ca259177d3f7f874764dc234a7738f871aab926c21b074d9a60abdd67cf4c74559c1a82da64d868dafa173eca0d668d60df01482aeb5cfa6c735ce0c938c08eacc3c8e8001677f77f82074229ddd39f5695f8c790cd2a0fd64875723a1f7ea3faac33bd79ee0cf0801e7e6089f38200d18e90d9e2b51bca01d6358dab3da3178ed238885e82880c5b8312d70eb468395b4d483c05503c2f190317abca38d9e63c33f7aba353c992e6ccd9cb3d9ce27c22a16b36478b6ba394a771327679b93c5d322060cc89947e0d9305edff8c23b73f0d29c2aa8c3c3850698a4d7b820ca936a363619a291c5e53c0195048951ea55afe964a31277edcda77f6340227e5c6d20c02856e714b1bfc0fa7bb2918715e5c68421a1a8bf187e30298ebf3ca71adf8d80d77a44e96d24b2624cd9dc0711d7948411ad831ce270a68c617e9c5c44398bf74c4006844ecff278624cfd277f4e6b727ce481552ff6f3033c47dafb0bee297c3517fc4d70aed7ee0ad5f5d38e1f662accb30c2514675b4a7d04e369a0cb421546a9017a1bf3285b8ea4c8ecd92f811c9525122318920c8b0445146dae9eaad79a97f5915ac82368e85a4ccce61d5073d51fad9dbcba73ad228907ef5d944a720663827073abecf98e9137e500c13335233a98b695ea5f9142385c3b06805f4b37129bc2dfa1cbf20e4e9cfaf24999b42f2a582f6f8565ad6a7aa3d31a8e624285792c6af52f4ebbb47bafd70b86881e90ea7de0f552a88be00d1c628daa53cee489b00cb6d2dcc740d5fc5cfb41f3b41a59edb7d752946012bf3d84604b541fe66f8402c453541dd84e34242401799c293741ebe7d0b9fced1770fb9c8b1110bb82153af45f742fd46ac4322941ebd88e6662bd3618566fc1af99f4867823e9fc15b5d5e9d83241e6d5f4687a9870a2a0e02a031b1b06e6642c2621148f1239fd61cfe20073e86e494aa4ff4a41b9e428ac2a7ff428038f7dbd3341567d5ddc91bb19607f6c2a2dd42670451e4dc402c797a6717c5230a704b60759a66ede9e41fd0b97fe7a3566ea5d0d15c67c8bda704797dd48adfc7ee7a06c9f79330c232c3f3bd75958e16d9c0a4e7f78a13a5e5ebfaca3980d7e5f4cd371e28164c8689189f410d1b1a0432f4ad81480cdc2606f5d1fed16acf45193661fe70e10c47489f6b01e15083ba11cc11da83970f42507b5f58579ca2cd8d1f9416892b267a2f57237d168ad25cfe986631834aa238f069723e7cb1d68818a86a753c858d00502f8ca946229c7e59a857ba030d9cdc08d1e855aae7f1bf429ad6d61face5aa8deeda8c26dd3e14803082a3d60ba249d2e09a923f073d1fbd742b5d1aeba92c2685b29ecc8f9bf7475ecc715a8b475d1fe1ea9822e8becee2f51e24dcc88fcc955fa41b1631c553d92cf63fc3443a4ef61cee15ea6ced5e64f48b8521b7c71a29cc08481578b4f159f296328fe05f8cdc19b57dccdfd553e6ec802d5da43e89fa4417e30e81b4c431fab8f8171837304d8b6e00486f107b9a03f51bf515bac1b62f18eaf31b5f840e094f2d55206a966529c7f13c2b79137f31b2545e8210f44e28ca8f18f7c3c7a857d39f72a8c8509d0feb0917c2e59c180720da8e89c2489c15aff9d8ffcf163dcbe2e7f8618454d427e37ce2c978ba5e8cc9f05c3045e4f89c1470b4218c24cc5737dfb83f51083f71ab669d11b8a8f84d334f6eb31f79d71f165ad1116b86b6ffb0865a6dfb1d40d72a7ac05b67bd34dbbd507fc79ba4723da2982a7a1a937e4616b0ecf22eea3b377d97fde77b80da02e1e6463355dcaf03b8cd3ed1d2653782e3d2005e18ffe9c7a21b9e7a33e8f8e9a349d383d70ea9b496c051ba986b7b126846861f869bdaa662dd6faf0301f91ad5515f13d924b247e7d8cfd8898145738eda0b33fa9966e45ccf66ed3dadc4b8858ef326e6d1ccb3c845314c3b4350bb6ff0491b0fe8ac002dbd8e17ef9ec8a079d211e8fa67e393d5aecf23c25b6d8cf74a56ff29f7dc83ccebbf072a6c3f88a0b4b6100de2b0629d07584755eb6d06b2d4624ccb6f4e1eb366dd7cd52c980800fcf27441d29559e5144934ccb794b036f5cf6b20b3e3ed80282a77ecf09efc5126a9096b99a88d062a3009584945bca4b2f8451bd0841fa243c8c5b951fb65edb0b7dce6fdb6fa565259aa4e1ca03d0d4b66ed8dccfd8c4e853729cf9059038b4b1addc46c536a42a7254da256fa24916e45109312ea99cacf4731fb190ee2a3c6804156c6eb684ee9167bb86804b4c8e2aede501366e4467d78c9460deeac5acf29bdf6bfa19239f595343a29956760936c7cdd5b345781e08ba8667d3a8df69e8726af4568a3f1a14569f532e37e7ff5e6f906a89bf6302e6cfb8126264ad133a01d901dd5713c1fee15720a9a0a97560a2db294cce7e58a460863478f97b808ed9d44127b77e81d261e9dea6b0f2e939ed1a3b8be80ca3756f1c3797906e4d5a46fe5d368df1582f32ca37e45e96f62e3fb76a0cffb4551f542dea3157abb34ea4eed65e3ca3d1231b5bb689beff5458a853b464a03156e1cb4de232317bd550c9e272152e7ba6bfd903b73a26da7de188c572125614cfcd404e0f94a6216c2d8c5096bb6902b33753363dc6a0d1eabf6e00b3087598eed35a7a07596fc54d1ee2a25a1236e64b7fc2d2776c2edfdc5e670682590fdef53125d2cb633cf917e8bb3e80feebe2b5b26cb1077839efde8e63430d19ba1e25d9d0f933299e3b24ef4727a139bb645de89dbaf78f54b80a672f32d3172e7314888dfee8bef96f493d02c051ab5b2a2edc090c06432aae7db660959dd12861d311225723c90dbb56f4c4fadbc00c753588039f5c9cf5eeafd701188210f295fadacafa0c91044f7254b5399424825f9f406a1befee89a0e16a96c6701e7d9b30ced0e42e9e5b1fde3406e286252927580bbbcaa38e2a4bf4f06c844d71b5b80deb279f6063fbd5f70166f675a7535943234cc61d88f1bf1604b930c117e97889f67613c0d65a1b4e8a90204dd9dec5c8f00a91f5203533dbd5f1d086ec176f45175d13454e1b389e9bb39ac6a0b1f0a21f9a80902f1c6c383ca99ef745b1ea1f4e685b15a451469b8c36ddaf6a5810cdedf340ec7813061f5cfcbcd9ac99959a1bde03b313b07e5059d2cec51c6891d82b72bbebf6e60308e8cf3962c40abbcc142fca296351d3697eaa63ec7043781bec795986416b665de4ac3ef2d088656c28be4c65aba31116aa30a003485cb17679fc2f47cf2571a89de7d12bccbf91a8a245482b93fde6b18ff5135b7972d7c56ec4d9a002fca1c5c277d0962dfc6afd782e574fb1b36cde589c63a5ec8031379b8ba66f94b2c3d8d01677904109aff144fce12fae897f2646a8571b744895efab7b13ad01bd722502cc456a1217d3cab09392aafdc0477a94175dd679de258ae8ff6fa07d7f521689ea392d65505a3242392142f37e215f0eb2d49661f69770ccdae954f052a6f99d2e799d913800948ab6ed70bc0393750b575e316a7b5057f0d570b2f8dbf4433c4d058b8a55831c8e48be1dc1417f0ef4b2241f9d85a5dc81dc257868aac9a5cb9afdf3017488273b76d288c37c98111716d170f358f568ad3ee2faa63803e22c9ea8e37c6ae717e657d9da5b0df8ad8c1eb67cafa5000a1a2a0d5072b54fa382b0ab40c49034329b1e2166a8ef094fd1eb83a2933d6ac3c009c24d4759dbec32a458d4ebef6009b2b280ef9d282749a94207c9188766ff19822779316617ce1540218d70eae50506e61251d7a4f8fbafef2319894a7bc8f21b5c47f47fe192450db88dd6a2f63e0b12b167732b85b0c075de4e74493344c5560d0318bec2964821b911eed9d517b2679602f19008fbf4c717eef68c3e2a3600631e28b88d9e45f79d4033472e39034c12c5f753e3c08b56dd00a55dddbbc08a134e1c1907f78cf9d82f7a1b26dbdb15e5bc02a673ef31ccf582f9007e0d4130d6bb596c79eeedd89d92ff3d0356f7a64758cb41057b8be0f637502a73116605ff4f2fe6d4acffe82d3e8e500a9f325ef3eb3d28316f844e1404e0ecf3c92e6d7f580cb3e9e52461b021965c8459862141d7818ce617dc68455f1dc19f4619be7a221685ba4a44b154ad59c27ce86b88a32d6ffe07022bc14e8df83c984cbab93e72e9081344dd5336dbc8496b62f0141749af98c9fab39437cd2a6b5203b1a0fde6f5c921f534242ec39e13101617658b09bb14c96e668c09e6f2a65d56f03ad339a14a2d9df0ec58fba8cc7f49c48bb32c87bba0f150c2c65b60b6af8b905ebec51579758349924f77817eb89a9a6aa73d383f664180a440c501779ae9814f003e69c6c762328fe5a06df904a7cd8f0960fce31192cf25722f568c8839f9bb5d0f7e1ab941a802c5e5d8dbeb86f981c70d6556b8b7029f2364af93a4b2c3af05be9acb203ba10744c4b54ad0e79614875d714b3252c2ab11aaf1dda6bf7979ada7a8bf47107b7cf0fc57834655298e97ba471b53b302b18f8f360f15a5dd9c291b3fde3dc1ceb4438fca13aad32362fd454ab4956bf301e801a5a59a40fa24f2e4cba761af8e2418037428d36d6685242e623e9360e2f210c8f5354f3959c4ff8696157b90aef3e883452a9e653c4c49ebb22d8e571cda2a3f15d94b94b0712abb3d8720691b0bf3afeb09622e1367c6244e07338c3be4899baf766c277cc572fb7262f385b1d17a6944c8fdc124722b7f75cf6f0c71e6c60eec2b21641043b7b6239b3993426623995d5bcbd51f64bb2789e1228c60a90e6f041e53db21be4d89f42dc9eaa0048af2f11bf235a26e50b8605a008ed6826a4387cb69537853238725ba523de81d44ed1084e20a09a1a8c5dd78fdef669cbec79e3fed17356c0e1662ff1b72ed90f15db2f5ae9e67600e3c61c39fe55346818ee0af00847812f4886b18797b923a23eb8bddc9328073ea8119f5d49c97f34f5d61b319fe285322028b3781ede2988906599726fb22e67eb18ce8aa8f85afa49cc702f2ae4c09afb445d8b69f6f504858a9b20e8cd4c8984254337d6cc9336e279556edbba40e7947f523dc96d0ab65f1081d5e367426b67c80e68eab594b38ef8bc70f9d096ecb7763147ab1af81ba0f5600d6dc165bbb848ff923d65832050f798253e1120abe1bef904a5a18baff8e9393453c53c6cba76cf708e607568f6cf1775acbdb240c5077e04fefc631d8da3e2db22f814828fce42e9e9fb4e53751ccf69f63248c6b5dcabe88e3902e1d59b41881a021e04200157b99f31056fb4498c4e177ebc28bd66b350a60731279eaa9a5bbf9b4f75898e420adf9b75bbc3841ce67497bf8d2f90ae57280cfac76f4c217ba4cc3d4cc9a9f44aee3472da32a480af066db6d8574f3311b447c81711544019206db85c5159148b27187156db2a70da03b371f4ab38f6643a18b0ce19ef61fca52e311711e483dc1172364a80f0c7dd3a578914612e526cb065374e331fdff006307364560db13fdbfc586d4ce13d50db00807ba538d834f5342e767e19fbdc78feb0a4b19a0193fd8353bfb9c875d99e78c98a8e0d0377ea25b5408bcdc5e73bb1ee6f9e765c24fdaf826f14a286046cbf3e45ed7f43467a4e3aa756f95a215c5b78b8c86bd95fa8659a11fc58c2a14cea637b3a5e3c9d6221fbcce8f8bf6e7b134893ffd02747c704c875167d5ada49510b6f3136316924cf1658581dadc185cc85ce539f3e97bfb97404034b3196e43a1aa2c22972c5f8d06b4057ac72156ed781319e6ccbba3b5d24008498853efe171034c1584598b75276114b3e31ce8763a8b591da868803b0d31b61b21e14a270e786431922b1814c2a0f0692aa8de7484730a1627b51ca38bae051b1b33baa8ac8f79d8a12051eb5cc33e13c061cd9231b2871f0609676ee4360fac04d3916c023ee9f21ccca801d75057a1506c7ffed118596c2c8e2a0fa5f4263ffdd01f3099643287cea06a2501f7dfe68a4c8617923de122f355eb4d0d6ab614715fb8c49d87bb447e648ef86b510d9c80115061d9dffc36fc9141b48533e4742b84067cd593e18b560232b03f0a978f3ea746256ce7c626b141acc0d4bdb679979147e61ba0b676a688dbad0951c9a812c63fe9b51d36d994d5b23941b4b51b34510f4a15ca89ee14443eb53857f2f06ae446d3f14c80673f5ae7fa3579d5bf20245e0356eb974d8b178e7a2ff9306681d8c6444421b7ef9803064825ae08ed58a591720af9f7585bb85810f96c5469d10446fab90e8a9a6efc6fa972223e424822df61b5c8f28bc7290b807004cbb00f38b54a2cf65b0e00fc15f478e6e68c9fecd66c9cf358203446563f418a1a18e87c818a0fc0dbe20992a4120d922028be7e25d18d17ab207d41fbaffb4b68840331ba4127f2e1bfa04ab3b23f9a252746d23b6246925efc6804273db7b9bec0580c8399e21c56c812495198d20e74267a4f23308510300f1ea051ba818d6e0b542b3c39f98e990bf55faf3904e4164f9a1b48e914c162dda90c349c89261d3710e24d972e29b2b2ac075916e81093672043648f5c13ca90af1660d7193d7b4359952678d37e8445725e5759b15da85c556ef107c159f563906201f31fa775efbc55915d827e2fd988f98ff8ab2cc88d768862168f5fec3a12c46c3f19644a969186cb8d157d18f47999401c0c18d7ec20cb6f8c36ad59134ed8bf742228646c8e7246f6552ac0019196d777a7c44f1c10a805270439934e3dbe1e2d01f1e3592851d47267cff3377b27817e83a9f37d895fb24aa82e82b73cbe108cb7b310ca310d3832a520ce81ed212cd9facdba5f8eeaad4fffecd75787c10c8755d3f0538efb69992fb583e40773f0ac1f641489d130ed1186088ce54c2f4a379c99422b81ed23abe2bdc5cc6ee07a8c411642e549b95666b5f0e72e563e6c498d685245e1d8e24ffa5ed41a056bc37f7a88480504144f06beccaaa5960edf9a3a3346d0d0d48b07bd7c44297db4be37cc479c8d5ed67fb323f1fdfe0a8aeaddec5dd300cbe2185fb42132abc55340355fe652e5b5a226fdda5f3b548889720e071add0db50ab4edebe3186e2ca836b01ee9b8002c4e70efc7267d6808bc71d2b370ba956f16496e83586d6527fe780a2e0bb31f4afac11c91dc37fa61d6fcd1c964f8579230f8f2867ed2cbae05f1804d04e81c8eea32e3cd0dfe446263f0c5f9771c97b27fbd75a9015c686e3766c8ed832ec341e18c905ab241d55be778d2e53a96ef21a341a2f40253ef410c176c4ccb6268d0e003e38bfd194eb1b7af8cc269ff6c3fa7cee1a83d6655039bbc6474c2aa64dcaecc9b9abc660d63ad974537202a4e856cdc579cada3b26ac2bea962b0898de4788abca419a4f5c9d41d12e6a67f85777bbae89aa95d70151634f250650603c01dc7d7ce96646b1e6cb630e024c40ea6d1dfb2199985fa912072b32e94488b17cd038f30857a40602c136b8ddf7af0998c28ebe977ab17753e214840085adae75a807c357680c70296bfd68218c329d3d91a59290379e8280f23d4e84ee7e7fd6f7541b8ef8f4e10542a8b6af9fa46bee1cd0d2a3be1d62f66fec593d827b8754a7298a6deec69c6be492c6f3f827a8b8e240fbb2ae15848794ec6716f0bd4578dda26e42c3fb806ae65e1f671bc0cd87bae96c740842579917ac722f32b2feb45e362b061393266de81241a4eb3c0847ff3ad609fa2d11905e3e5d0c1d6437280eb6f8592b7810bce750785297f6a62ee028d92082d6dc709980929fe5d963f7e1bb199528a8fbaf81224ea02b4c8699a7714ed184b4c25dbf4469e00ff5ea2f3660713771d9fec8b4a772bec7436180ac923844715e5e71c00f1ac28a6a1c274895e74760409c24152eb3768f2e07c66376634d3fb5c23c728af5540c2cd86652b0c118a09c3b0bbaf947dc845298b99b8eee60741d40841699332dee7d9a430c87b9006fc330c74dc550833f595c49da03dc7efa97c33c3f02d4ea23a80d73ede0219176428ccdafa232336c28d368059e2373c25d28c88ae6671638f037434c76b325ebb5a47c3cf642aa873545a1d75ecec7395d16b3492f9e7d9343752df56b58b24b87c44053ae06a4ea065b96a8b0974a4646a25ae7936c59677a83bf0191bb53a01317447c1e331b06f9d6a2c7d391cb0fd7be0a77f7b28cd3f03daeff8873d95cf6851c94e41571601ec1e081d2033f9cd91587eaf58f965e50cce96d528f0389cb9faefeb8ddaa98124f107b0981a87cfd7ece71f8bb10b03b8953188f9d6d4e253337cab8f3ec50d741e8e63ac235d7dc589afa7e0022e293c9d48f0c99a1d17ca36ab2bf4ad9a1494ebab0226795146b6be7964438e093b8ea5917a0300a2734267e0d6cc9d3e6d03135522fe7183bb6d88fd34d5adb32801193a4383459a262eecb2d987cf80ce64245346dd76a48306a49f5cd0dd9e28d8e0db0067a00cd3b77254abf6c9e6d262266f222daf2dd6bf547bd1aef2c1750db0888881966f905a9bde90d2b9b98709dad0f75a8e93514a39ce5d463175fe0ae4426c7124fd6da088ffa48a7348d65fa52bc8f41d62413dec9bdb2bbca538aaad13e70d3c04566bd8aeb34f460fbe7aeb086629b83fec885d50c9aeca898c868be0f7c071e42454888a99b470e911ce04100a17a4ed1db00e0b85c72da6a060c3d6df9330b7d54d9ea4ea35f377a512a93290da155f3e1ca1aea222b30365d9bca0a068df4dd040a10e463783b84a9198f49103aa13f08a24f602ee9aebfcd75ed9a31af8125dc49d968acc288d14c23d74f27f9daa96383925da99943b75b46f07f9910a9d9855e083cc3a82d04bd2d56534acae7a1bcbd3ed836ea57c76c79818a60dfaff8ea4d3e893a2787d1ec8059f63ece92a3283088793a07f657278fdf39208c4820e62ca0020bab62f4162e070a5061c9d04356d67e699f517df25f843855b793f8bc2c46f076ba1b542d0426b02b2d0adfecee2797f4442932e60cf5fd8837864bb529ecc5eba81ddd75df41188b94a9790912429e3c4b5911abe16451084e34eb9b54d84e456270fca6af36a88356513fa54a61c4694a20d752718ed01f129c9a766cf25e8d9eb219f80419c9f04b610a13abb312bce3b7dd4b04152035fbe28409b86cefabb568af95fa7767d8e4434b35f0af3656024491156c71d561894aa520966d26a0f2e77abd2d98937c1a9efcf1cb4a2e1a263de0e7b0da23a42b4f253ccb4c8b2b4feb98033f233bd88db269f26adf5e6437e078b7895c3e3c70b312695aeacc749732f6feef334698e496d5dc17864fe3e427f3a9b6dd2e4e7937d4f1180f4ba65b5c8f43bb24cc96dd788cc000e9de4cf897485520aa60db4d7dede05cbd1ebc70e14d46c3ab16e3c27d933d02e45cd78d75fd0d4f2fc50c734641a4f2a15a86b33e50cc9efabb3f545a53944f03861df3d89f810efe1ff148b089c008faa675011c4df916409403c246e1719aa0e51c605829554c61d1753f1fe33c01e531b3219c0fd16953c6682c6187397920fd54e3f8941e04b32ed96bd9e4c7c1ec8d73cc128e4edb3345de69b6095b775e7bb83f0273fd15a60320c46b154b466214b3c551b5173d3b0c247d184e6fe33b3b9b33e9d96e06237deeb9850c4263769ef6ceaa8867aa86aae0d63ab9d62fd93b3e402c482006806b67f14f14f74e37a132f4e8ee947f8840394ae7c70e07d3c4ba409902b5095717449caff1dc5eb1481911bdd287bf8d72615c1e4062601e469e02ac159f3b8baffb94749d0181cc676510fcd2e5fb0f04a502639af27827546089f75e284ddacf0370e5572810d4c3abc8b80a52ca27406a8401770b0c7522ef6749b3ef3f36c7711dace3ec2d13f4f560c0fd49c199f35e23f55be361d27b30214e9865397a34257af11b6ff72c3f63c468b7335d4a31c01dd063273d7df0b1ebef28aa4571eea94ae4f1f7b36d70775da44138e6974bb6ebf336ed0264fd374fb0fe9ae7ae25df745e2570b48ca0642360fd84f52e7e6a143a933b2d39a6d2db7dc0bf56f0b348466a61846d86852421101ea15e8772587476107d8572f62cd0b3f2176ac39aeecc8e8e1852c7e271cdd71840820da1d723939eb7c4786a0b5885e83c23d01007bc05ed33003d2e1afba78a5c2a92edfe5dfda8acf2e5dd0fa2bf4b9fc2e8c6e786fed3b3b68a6ff16c6d3b213109b8071d256b5a08f23b134fcab4c9b444f740a58af41d3584426cf88b2f188f55526ac57dc893dd7b756d1deca7d715df86ccaa3121c2fb7b43cc82e9ded7d4ea16939746cf93bd36fde210a9c47edd06457aea6dc22e6aaea6c8f1f71893bff2023f43a053f6f3c0b8d4d749b1b6354d3208e7efbb3c011434f8f372240c96577dc528442da8f96291db979816a359f260ef3a8dc97ace49e76257b704923da7c5b7b34a30e4c135e15e90f74e7c812901b6a913dcdf2d2d599aa6df975e482e7ad704069fa9e51130372562db4f8aa3848ea27b019539e1679476fedab667ebbf8153f7c1ba6fbbf9b9b974f17a62bd742d9f26e06a37708e89eec30891dbf119b2b329329acb91044ec0fa094bbdfec6c9de12a230513457789ef47d97d53e4bf4957b82eee8bae54534abf9ea871c98d01d724fb5fd9d21b883e79929b3cea1a0a280a42105482ecd1616e9427c7b329a4b49c5e3d13db0bc6a7501ff54b263e5a7519ada3bb14869b4eeb1180d8a1c2c803ad2344e4e7c2043a5477acb4ed1fa3fcac1ec6d2f1d0f579f66b57004e158932beada846bd9e5aa7a6075807a520c1ebf721fcf058777c255caf6e9daa5f31a2bb2194a4569248159631934c14b2bb2c80bf0cff14b2130732928087b9106ca6f1b2885d6b004656fd650f9290608e7a74b0ed354f0c175657a6611e83ee86ee30fb520c481b56d08b7981bc41c7eef13722ee82a1063231f91ae4f05adbe5a14ae12a620392a6e93fe874d9e124365ea3d560f22b6a380d9342141d113af91774cfe2f9fc43862600770b683de20a4ea3daf9a3254fa613b575e131d9e5bf35edbcb445b7b26f5d7611365e3b122dcb067e950071b1bf5f7ae58abfecc18a5ac6f6f859d4ebd5dc9ebac8bfe20b33189f9ebca32d2563e3a4c56063c9680b4e65a41f4ec24c0ee459fd793c68476ff41ec7fe351b71181cfed0349f1c0bc6ba0d1e23ddf249211ad1e9bcf32456a5688941ff81cf440f2bf6c75173cfd1b661d06e29fe61336f672c3e658dcac40e08387e927909afb1fdc260096452d0daad6c269875399b714487afcc6947b5c6b86cdab4375340709fa8d755d5092911d68087833a1dabe1091d38678740acfc14c8d767f803599a9f2bdef007a7e35e296efb76190621fce5b44ed4be8e3da3e5ccb3b6acd7610d2b26815ad83f8a6baaf52ec969025ed621c0c730b9c548e1ebadd76f83090e67f6453a9d9fefabde4b18e122e8741759d2771cfbc7ffbd3b0c6bf384889362a32a50f2622dd479157820e0a9cea77805eadc032391056943cf9587f97315ac1ade71f458318c917cf20ede55456dfffe2187558b25fcd42e910eeb406b2a14c4e7049d517e0dc918df14ce4426fe9a756dc5821c0a719bcba630aa600dd789d97883c5aefddfec472e8c462f825e5605fd2d8343022e4db5a981e14d82d09a5ff5081d3b91049d9ab41d0f051ca7d12fa48c3ea03fa9cebeb087f0270a024b09cd3cadc8f842f1ecd9bad968b46eeb561557f7aa963b540fe8610f031a7f3f62036488fe6a7eb98cb145d395b2eaeab4674ab4cb0c6054da31ef48d4b6ebf466cab1f902eacf917494ba8735f54b22f9c51722f4614421fca9f135d567a7d2fdd2089520d07d2a524354f495ef60d27faf36a82c94f8b872635fffd691cf66c8f2efb6efc7141bc3c85c37cbd5c2e3e4a6786a41ffc5ef2f9325d994a3b54162c047206da5314760d80fae12a8f46c5b7196199dd314feb6e40a3b38fabd1e00b166d527789fd896d663df6332d43f225f906dd73a180d0798e660b545069967c3aafc684a70390a6d700472e48c56d1a46a802b59bef418529b2fa06949d18d892c3b9f699831269aaefe480708d3eebf92163ddd3d56ec0611d15c623f0ef0371e6a50f1da8d935d71b189344661c081966949883092448a4c6321ad630bf21a984fb54e34fe197fa611eb3ff2ba63399b227a8cf35905d6b69fee8d485f8eb16e3c17bad609b96879acc603e9cbe44fa22426f1faed215e8fc1478af51300fda127acdd2269fd007dbd6472f68cae68fed6d27a29c1834ea8e012aaa216745f5938a50da3ad4cd4bba496cfb4ec70cb3d40fc1a24eaf6bcbbcd6b543bdf81b6b56863868904799e03accbf0ca366563a0db4b0f2583d282508b58a31ab94b2e868a68deaeae22c74fdf5d9f745132e9c827d0c8e1272a0ac720561383813c54f8f589df56f34aa8821bd5acf54e8971ad5fe1e4772e55e8e4e2c48593b0a3ef175bdec7bca2a61fd3bd97554d290d4df613d36bbc6fd44af3c8cb756bfce7de147c2161c9c7c7eb8bd093ca68a88d85a901c50ae02a29fd62b67900c7f6970692800a491e32b73fd6f6f765fc9b14e1505d335f7195537c3c2328595cae16c125e81b3cef023fb4e6dc2a34bdfb8977acc986269810edfb233758621f1acd95f3a4f19c21fb2b709a8775003613a106bfb6ca7d88432829779cf8cc7c7aab30930afe8f914868532e50384cadf7fbe37cfcaca54d11fbd4e623c9c126b52f09ed738bc0708c98ca9c7136fde6317e746bb7eb0c878faa1e26042a26261d576348e14bcf21c6eaa1934a8d9ce22351485728a42831541c94ee62b7e913d9ca672750e46352f38fe5f359340d35e13d5df5ce2dd834e7db73c1f8d0bf78aac64ffb470f2ae62c270ec3faa3730e8d4c09fb3e366725c5513b773b0436b1601d99f4824ead24c53df9ed49df144fe33b4f4047795333054641b4378c8171b2ddc28901d17ba6200bdbcf21cfdbee4efdcdd69434dd4d61714fc18de39f7ea471eff87726c16073aa38b248fc14870207356f9df4b04a57921821e43cbda859a2a69bf3a992aca43d6394f249e16c0a3f3d9dc8179a8228adb5ce875ee17c7aa91599f3ebd05fc825b03535e86b32fad2e6972f6f8b6893f8680bc0408c16ca3f0db63c238a7753644a84b29b5f3f9f67ae8ef65427aa8633efc6050eb8a6bfe3dc2937ae7e62db673a7ac703fcd3d09928e742a2fa92cdff792f3b016a3b2a4b941985aa28a039083a755f28764412cef6fddbea9892377266e8ba32ad0f801777d23c5b0e29abd19f86a03bcd0d3986260b5157487d8507e80de6df248dd150e0819ebe28208abad60e28defbf8e8aee6b968f85fa14b8e33a3cb43de6a784b8d5e05faa40e32c60c473fa2f569289e714ceeebece74102a69f72ec0f41d21f27f4b4372a09fb2f1ce0d7530d5ef927fd4019324b7fa96177de591bbc72e32afe42a304fa8bddb562040d4208232c8ecca3bf4bed163181b9c82f3407a0d8eda55b5efa47b456e4b50292ffac8074e002677ffe105e2af8cab6686537164a55f63c48fff4dd9e9e73f80f0324cd0b872626f45e24eb688b0ed305a79d4c493e605eb8dbd347a9e6dc06f7a5eee372182c185d0fc0f5f847e2f598fb712065638966eb7203199370df7d3032dd7e5b50960036d83abd44c36d8c7e5c24fd52b5f90fd890ad887545a12dcaf472e7913dde456496a46ed89de63570bacddf43bc8baa6bcfc46c0408a399dab565c7fa4d168d4bca99006710fe185914139cdb289c769523e4d842adc37492d5b33c31f4df5024243e5bcea14a84ae587015b2ac6826f6adc7a2596f609fbabf9fd720f0abeb2cf12c7fea308b09bfc87aa17842526f95127dd043074a192c5c9aa1c2b7ce5a4bf64265fc555a83c1b695cf52a084b1392133b193216ade3d6a57e84ffbd615f3b935483578e69b90a5a2328ba05358d203927457d555792dde67dca1f0805cc82a1c078888cf9043377069fb215ca706efbe6cf0527843f820e6d2285cc22bf571b9704db714a8e339ee6e9555d1e5c579f11618eed264b8cb4cd06dee7cfbe839ecf8a247eb4b1b520a0e1628098023759b7ca720831fa4969c029d6a497f632df293bbc91dcda0ae5d11ba645e5c6d0b932dbf68e30cad420d6de07a4c6628bdc32ee45886fd88b93b884456e219e99290a64970d99fea2aac82f3b82968cad0a1128fe39d6af8f156f4e0d3fb7fec854911aaac70049532f3c0aa81dffa56d592c02ebc8bf142b8123f63adc1e18c0c464bfa192552f5dfc4dc7d70ff1db519684f80af5f2088176746aca8361eb925c16eb91daaec4656ae8eddbd9860d392dbdf54461e3d48f7e2283da7bdb91ca532d0c456e706c72e6a6ccd1487c36fe8799ec1cbf0f52adf5eddfe03bf814b5f16806dfd60a686d1a8b4c581818a074dbdb736d78aa8a0812d5c74eb67829960a2d23366858cdd49c37d7a0219bcf1b1b08e40d3cc010e8757df0d8348b45eb517aaae5f36d91d1451a944fa122e7e0c3eac3af3dd61992cb0e56168ceceff54ee9faff78ce51eb70863892d19829f18112343752dc4592d96211bbbad2ee377af06e607a52e56b617f2d7539df2a71f7e2289e001b61b8549eab393aab4a888571bc27632718ad03ffb74f62fcbdb51e62f3048aca1448ea952eb04ca4c2540fe284b795b2f1e220f75c062fdd8a5ee18e0299a67daf7f3b94868b2410dee211e1e1703d77f4e1ee2d984b88d6406f18e81df6837f3632d88e0834d2c7895db4ce41b1af81ca480101392435f8012f0316b704166483a3f6c4c37b3e26e8034f50bdd07b0d67b73872114911eb82d4c1557e521e095c569e1e76bc011da88597371a838f52a61462b3cd96019f19871b5c59789cbda0e8c06b537b9467aa287dd32bc6e6b6de918d11d83f7529cd7e9723455294c24cde3c6a7a8ddcbf7ac64379ca40e9f3a1d62697adf5d8e4bef632fa89c8403cc124115dbe1d59e7ed32f0b153c84548a1f4c942d1e4324de4b4ace354f5c8f594a09a4e8565249803b73f3449e0bb046c43d2498eae6f89866edef9c2357006f66ba4930a124a1829dbf02c146bb021a7a5c37b083f97522615c59f5b93ae78fbd419fcbe8fe146992482ff836c92d6ea270b6fe1d53f248da9bd9daed276cec64dc7894d3a980b1eb6325d54cb12f7f990ae83fdcc88285bac511bd423c626edaeb5d94ac144bb6ccac54b080a2b032d548eeb53f4686d23e57969a5953308b42b909bd76625d6af7a9ec921e05df1071ae24daae07faf29852567fadc4527913bd4f7797388a56b8a1ad98c94aa1e1fabe982c528a111b0163cea4dd7cb4fd80f32d75a94a7d03949ace2549e71e07dde36dcd857f4d19ec33e735e4d7cde8208ab17e8ff63c5d77e795664be55ac934a6fa4c4a6171477b70be6d2737be3b96ef5699a5a8baeaee1750c38bf59f973aac30d5eec21a72397df07628b69bfe23d4e117d8001a59cb7d211acc21f22429aa6432714ffb19ed6aaa91bf4c9b28f8a65716dfce48a4b820dd18582b85b813dbe2079e0ea3e3ab2dfb9ed187ae0eeb0861024ec79499e0019cf1af2eacb2c686c046ac6d018eb2182732da7a5d94723e3e5facafe825ce6a8b09fc724de897d556e03cc96ac1ffb15b5db3f45869128424ff24444ba7555e7ffe16db96d9bd16881c0ccdda4e63f76805cb4a8ba654dfe1c04285b6549fb240df9c161977b3930cb5adda0757195f5259d63e479ca8a21a5423e59a8855aca9eb96a1a1faf149809910fa5417780f9bcf286b179a4feb962104ae40d940e7b369d52809ee8e2ad3c39b81d07c5934ca611c90b5ced44c30197a543fef606fb18d3ee866c9b5eebb857d8a5b291e691120574528241cec1438bdebf36b83d9d28247df03dbf531c41ebcb95f1f3a20b836b6949ec9e2799ac49e5c616e64a21fcec5a64b21266a308805f847e57223c7df5141c1e5f6915e333998649645dcc32b506a674dd2ab29605f93c332b2a21bcd13c825cc001f53bb4dd2b6114f47290057af84513545f4592a7e2fb953df9b4996fb4c7153ad661bc169df08f6b4d307e761d6ded6de4b28f69c5977868057cb975ac7a6e7973ecda112c93323ddc615b55cc87475ac47bde7b2a3fc2530a03074a2eabcabaea0bc761341cf88cce7b83c17c3bf78d091a387159ed0dbeaedffa97fdeabed82a03fc1e007906820d514d3988b0f53bfbf5c8120517c9cc429354182f843c90c4f121c646fffc85d71ff622b5047d37bdca87fdd5104a23d890a63e5cfcc7134a5631fda3fe534c85eccc884e7bba50030f79d9be33f28f45aabc2bde4e5031f0d762dbbf718303b7974ffb9f4ca843feef7c2843c9eb7b30335469e11a98abcec4c5b571d2956719893b737fe7106e838b8661b37da7f687176c98c2ffee798126efada969225e3e0f0ed213ace2e3f3f85117b41965223045f39ecd6f52bcc932a70038eb6cc475cacd18df1847ce1dc61eaa63f8e3bdb115cdf906e01938523e458bcb3f6390d02c47b6a59ff470660b6f7391a0501d03da233b93b14d5d31833118dffa63d7d2cc70e6118b099644c505242c35e3a3aa8d4ebac6058b6129c6e279d34acb261d5ceb6f278c9f5a47996bbcd06dcc344cf9ddcc5649c1032d5dd74bc477fc8bb1da7843d12ae9dc5fe6568fef88def06e865326f6d63c710a2f8c5ebc5e8e882b16660d13699917bc27e53cbaea186af0010212f71542809a4c108e8e2384e44473f6a87c58546ff8e96ba082629eacaed05d68b5034ed6b9356bd7a0f383ca798c7a2cf92f1dd75e1ec2fd194412821a23a741bbade3f9570f6e89e270f72779c05d3aadf8f63d1962d5343f213cb8e30c2b02f30a0701f722392f61b63a8b76dec6a3456c05bed5b7713b6a138d1a0707f17037da931ac1d1c468b25b7ea8cedab9f151caf26d7a616b52180bf7e06d508091b55480ca4563b9ca48aefcfc85c2034972499c7f1abfd218ad9fe59dd0aa427cd4fd99c52a8c698de418bac2882a1e6b5fb4ae62ec349f896db7ba7fefb2436de0e111aa9fa34b8db3c2e508321edd1bec32d2f03ebc95cd1d58358b28b212a99d692265eae9d4b28ca73d9805521b560c10c91c8e576ecd8094fdae8817b491b19dff429c9e208654571914f7192206f6ad7153b4eb3ccd66851035ec088f227d25a2cc2a2fc12c4fd06ae11e2796a8bb5832e55d48ad88229db27aa1292957b32dca0c5e9e87d8352d57c7750b7bd94433e8dc687e9cc692526c0ce113665aa9f4cc052ff9c27d6c54e8186887c13096da931a048d354ba0a4fbc866b1a7bcbab99e64ffe8ac84876f4d28cf94beb5e917578ab76cfbf048edb9f43a166214c4144653a24a0abf33dd94eef3df3e50e3403ad43ae0950c90f0f12c1a11524dd2ae89d566b0729f1721781d80e79c4d891679b0410b72b3bf1a4c1ade969e6a746295348a97cda43ef15b37ee189c381222cf35ecd83bc0d1f60288b257ca3779ec37353e8050882c13f9de4ea793f04e6e4768f29cae7493a5d8568d1917b3587bfdc25c9782c5774610adf7842681f9ba89c08b92747941e268827fc6f80e0be70db5190734b521fa2db46ce09b839caf5686b14d0afdd56354aa6b6d8c6d294fa3dc0de671cd89a762eb9bcdad806627807efa1931363b8f2cece3a6cd177541fd2dcbf8da1dfbe26928636ac4fb276a6a47ffd32ee2c3c4825c8d734e769d63f98f2b73287992c0c3dad1e4f60e925e603028127b88c07e1929c1ad547694e6824db7a175de140c1809910f5c7e1a218d325846846c0d6a263984f541cee74bc66034d813512ec0fbcc79774e3e80b6840523c2347e5777f731912af5c9abc820c7cfc2a65a821ecd8ec41df64241806a088e09d34fec375691fa66d3f6f7002af770515351974acaa225e945dd0f99eab1ebaf128c1dcf5cd391690c9cb8ebb941d4e3524292ec0cbee459a327316b7ffb30bb9adcd9bce2e8838a46712a4615ead507a5ee5fa04476b70bf22d4aa19d667753884e17032279f1d6ebd4bf447c97294515a62fb3272e880ca9108451e1930b19c0d2a907c2724ecd11b6686b3d32001e9f40f75c129b6386934cbc58cc07abeb41c81250f0e17dd9417badbf96c1009da3b4e9c51eef1c5ace166df5a20e207738b32b0c27b6f7ed63ccd86cf43e208db4b2c8ab4c40a1051a9ee40b9931dad18568e96f42c69719fa2eb04b16263358bce7eaa9d02d0df97e070b45674e9bb8f1f43d8d64ecf29b08c7a94a31b17348cbf561f67f0c0b9bf9e8ad9e1a975d8fe40d4a8ddd9041d9fff07f1a737d719f4328fc3e34e439d1a760c761b8ece30b229232bbc163971236334afe026182e26176bb511ae504806186e81b06f24a85b5b09d04addbb44f68d10f0419e0b0f169bc0e5c1525407a104afefef71e6ab9fdc2da8acef34bff407ede1e9fd2e6270ff94ba1b88bc3fd5273dbde3ac7524982668694bed4134004efc21bac4c497b44973b8b842adaf8d332b61f612d51fc9e19ff0df4672a9f41b075d5ff7479dd0e7bdf84be351928161403d857b7a9432a3a362940c3d4b8295a90a678e8afa2940f768d4f64ed0cc09974cf35e707b8cec1953280b1c66530a9ca8c8b9bc5ce036787f0c06afa01e8edd0ecae59ce6149647afdfe8d15b48cf72556d70777fca66d29bb95f1dfd19fc427506ec96b252c50cc2c1782b57e1501c4c7d35e07ce0feb081d6b83b5fbfc198a5bf4118a87a2de928c45cc4418953d8eb3bdc8637624fa225488215e5044c6be11a54bf4779f3896a44587d84407a2d8bba8c8f03214d396a168dcf1da7df3b43000c4b78650de2f0fc51fde324ea0250722d88ea06e807982febaae2f0592095f562c04fbd0d55d9c3cd4d3335919e6cfe5ac4457cf247011b9357c0a51eadb9643ad948f10619aeea6e48c626545386ec1ba0ae306158a0ac94355b720274f93f85c0190ced4e484fc09f3f285c9b0b827349ec1233da1287224671bc527aba75d4cc61030107174e2f09bfb267ab611acc551885f422cd0f24e4cc7512251775fe590ce09836714645bdeec3c299b417a5b581bb141f94dc6d5654ff72a14b44df4f1804aa05e642d70d77174d8f9ed3bc593b64f626002f69b460ff19ea573bdf4ffb0b3603b3cdbc9d91323c0f13f9eb1c8c560373ab1ca6e0b6df2f407671bdb995ec62536efd6a8f009abfb08793188bdf561b5f22c028693efe8fa29e7d3905342bf028697f002c71a52259ff8ba951a8b21423fe20a794b1d082dfd282eeca29027241920bf6742de5d4dbbcc97b1f8e6f95261841c1c37a9347639c6452120008567ba40fb60ab07b7246ddebcb0011f9bc3028b758ac9914b8d94406095762237f02e6aa9429dae3d97183d87e9105a0a957b6cfd8a95d6b2ae7a8ab977b04c244a684100b27f691cf33bb90207b27b206bd26b295c1ced79c44b64504b22602db328f7dbff6b9c5b75dbf1e8f9579f90406bced181f5fa0181a23194a81092078732d18d64294520a848628dedcbff7ca41eecf5d385410a78cbc61c7c24c1021e0c0354d9e00abdaae5ef0c76f519a07548b69e57c336a40df02408463fb53b050d1f24bd27ffed3471449ac67fdbb1a84183c5a5ba14d83fcf08ce0c654f6924984a82e76b94a2a4404c35b383d610ff4f7531cd484fac55aa410b7c3d44fca09cb45b1efd0705e2ada63ea4f5b2abf1f15e53bfa808eb1d020ca335eedad8a60905b7b825b3d45ed201d852b31c015a0f835be443f02e7435aa36f3c9bcde1f79c796391ed377a9f8018c272781235ebeb7d856a2d2b0f4a614817d65797a45b92f48cf6c94e7c10fe542f2c438a225beb63ac3e735672ed790afd09b852cfa5752b7c80e2c324a9171a4d7e50c3348aa1e6e5cc1a417145119e72e5fbb454f2a605a00546a8560af20c87b88ab41fc951d460fd436ff644717224b99338915fc974be88754aa1532f63c2cfd8ed8661fbf37bf7f016cce56aec9d22e40140edc2aa6e3fc5fd5a472cbdbc5b19ebc02d4b0dc44f708a7e6979b30a26ce0993c0a98579ce6f60e204cf35a2a638331547d99b7940991fc88d927f642c98f7c523e7a3ffe35750933bc4c4b57e03845c7fb067c9e42846d52b0a71ff032edbe5cb3fda88494ec9544a41bb61a8094ddbacdcc33aa5b78d903767dc7c48d1f0f06f6cb7e0069c9b4bc22069af33858a7283e72843696e810d40172566dd727ac216992e1681d400ca64461d16871e3e7e345f1d3d84ce9905d9cc8be636154c556e51b68e3ddb47fd0c1ec2fffbe3e3a71624c99701ad62bdd2cd0fa4540aac628c8914cfdd7feb1192e54bfcdd28dedea765767f111526e591f91b1032dd3af82ca6a8edc32b6a52f9e7bc010f70d9cc2125726ff3806203fd97db4c3946e58f358b628bc6ca7bf10643c76916bd1d3cb8bf3f21c6607aef87e64f4dae0216a83872f407fc1c080e46b6f5694fdb2af68ebf336b056f07de24948bd3725a1fc2daae45cf25af768324bd7ad63c16b21019656c078524cd73cec948851141f1e016bfe655201ccef7a6bc5ab21be4d9795efdaa455d003ac5997ee6f8d91aeb80fa046ee2c01ff4495a2bc4d9175db0c2ff689d7040c0bdc81993b44c4f5aee8e88ee176486e24925a72ffd598f53adb0ff83d1cb5b1fd03c1c62e446fd3e444b5bf0db689315e319847f02132c20ac7b43ef2f992ab257ce519a49ebb7c799e970497bab9a99580e9f77c0ea20fcceb3700e07b9ca692ca470c494267a86d0db9ac7e0dcd8b53828108d7841df76c2f5df25a3a99a0bd60ed15e952d59acaca0221637b0a0d6c1d4d1cdbdff0d8fc752948608397e1e49b4093b99243b16618328b5b02279cfffda422c33d5bac99361fee8c5bd79d089c586b8b44704f84ecb11adcff5843668af6d9ba9e8a855577b997ed73ad66baddd25da4e486fb8962c4dc36997992b6c152c9ddda19a6a914c9312c27fe5480e3388fd5ffced09e1b1119de7315330a414439a817c1c828317589fea06c454a816596021e4138731b0c4f1267f453149c11d732da3c3c1d92df28b5f73917ebade5612d6cf46b92165b8a49c000a453be079e4d4cadfb77ebba16bad6619a0646ea260df1abf4254a363c28a57cf081271312a9ac801d5423223fc8c340b34ac0f748924866c98f87be0f7de037c48ddfc6bfc4838bf27c632889f29cb8ab5bc1bfe05dd8d3f73825415aa270e60958a34d2224d4b66b15818faf709b6dd02fc56b68f7c1d186e66440cddc409829fb3362a7eb1abe95f8a8654f1d028b8c9baf35e6adae255715e61eb60bbd1abd82752c817bcb8f9a8de4bb09dad1095555a6e6852b4b32e45da022bf013cc166fe31384e07a18d86fb679f98ebe284631bff3929bc2c296a77f9989860ec0043ff675fc153feb3e058fa71064796ef1d7e5c654bafb97cf60b19fbd89c901c33c89326f10178ec0a9f975a1d1e35c4dcdd7099b849aa27f48d9538a8bfbb1b394f20f9ff80bcb782cef9f7109d186a2022349a799cf309e81bbfb650ccaa782b60e11908bfcc5783d730754ae312a57bce4b87e384dc49fb93874e42ec1c8dea911199c359c023222eadd0cc2813ac0cb25ded6e135633dbe620e8c92c29808139e9a8a9bb5234dbaf84d82d92907916ff9d1e111de5fd2c17cffbe38012e190e929bf05b0dd5f72f7f6b85ff908eb0a94403d1e4b2c87ad00bdb70760ee142dc723d5a1c318961af7d0dbb8013b4c25f3a0dd8ceaeb2129d1466c869291d16d847ef13b1b9de2e77de3cdf2fd3a4d1e36293fabbdfdac356aa988cf829cdc1c4c8125d437f6aa3ad6a4d247e0895c9a909c71b3d90738b83abe05dba5093bece14d24d559e66a06acbcce99d790aee26cc5f8b7ff4915b26a02450ffdbd1db95cd31af5c5b115acdd2be4a912b77a39c48e8be6689403d8e22e02fafd33ffadbb0396054059f2d1bdc330f66cafe2cc006bb9447add2cd2ffc9b0d16e7a35b2ea35bb79f47f543db4c8080c285663b2debc919abd65b02abfa8f05d8bbeb5b6e1d6059b1808e6143ae8baadae2a55b773e76a574c926376647275e0c2534e984c1fe46288b57c8df09c7c0d030797a517b47594d39fc06b4e15a2e9c2228316253e1a7fe138cead9bfa026a8b1589209f83a38178935d51ecd13ac499c12bfadc25dd18f259ba8c26258ae3c7a3ca6556b5360745a6e307cb85b484cbc9430fa0763d2b0911a36510751a1a20abd56233bab065d948bfdeb6e4fb59e3d7f0ecf8e5bcdee4cbee88e652f528bbc193b4d87280c0301d6e6e7301a7a9d565e651f8332418cb72f13f2801483918cde94d32b5a9e6faa326abd57f6127d0b4c2f68f08335dc841c39d3eae2a7e7f5dd1abcd1e032ea886ca77d91f6c635b7e55cc543ae011e49a15df292eda3275aa32929497658b6101f654ecb96b052aa1b2ea3dd187678a8c581b966033537d049f6b1fe73eb0de508ff3554dc488a10fadc5426806f4918c82fdbc48a8739298b20b10e00708a74290df1cbb2a818cb8300c73518b1b01e6190ea0d81d48221f4e498b58ad63147694fbf0b21febb3534cfe8a46361693f64e4f064e8c237cbbba7fe7d6375f1649901c3c3cc6649c64df28e616e608f9e03749a6ff7bd88edcf87ae2cb0a3b00d73c90136868dc9063ed036a7e9eb37d90f226fd4a126e2da9e9d4cfc401341d5cc665741b594c819884f32181a71a31513d2c9fb9031f9d1b3e40d4bde4f4ec633e8ba27017e8eafd35923277b22497b2d4000517fbbcc1e90104ccf9d8ae5de2f26b1023dbe189db629ad7083101b897c2c4af5d37677bca0350727a57f6785a71c084ac670c83bf417a5d1e5136f748cb70c18d0c406aa476dfe39bf7bea094c6df0a2db8bd6d43fae8972d0bf934fb3c056852061b9c058ff9a09a68964b4e3d375a21560f7eddbf2d9f771b59b253fd185e96aa6d2f49d1c2e44095ad425e72f2d050a044057df49fd3063f58f0620d3168f98b30e35eeee33b691d69ec04627f14102010580280ef06d08896ab653a02d4b0daae8e1bf1681e25fcd0a7e7f23c5bfb9495627aa687f2c0c3de71e9348da3de41591884c72c6adefbfe1228a189498301292233f70939279074428cbe5110048898f1f7f38d5cde48a7603c06ba2630a1109f8609377bd926d53e3596edbf642aecdf21e9626702c51b1156fc6f6e4a781e28d3979759eb25ca0906d33378a5e728ac68b84201e297e4f265b7727549933dbb1166a93ca0ce7335253d43074c567e6f4fdb99e89168603474fe3aa0e37d3bdaaa6760a8277b7188c781142db4e5602c674147b895437a73432dc080ba1cde68c21b05457a009d8224e6688fcb152bba47e507d9fe538566023a8aa24b9a56a9229e9dac99dc9080d90db74de74b7adac7f1750e7a22c2265dfeb59a9a60677c0b4450fccd3c9189f8f9ea154ad3ad6a4339e0c6d4a9ad535663d8ff9b52f2e7e74720e7f64445aa725bdc487cfd9a554de0c81d52fbb8277ff6fa57d5efc6ad5caad00fe6fc911ba94f132a6992502cd4db98b4bffd7f16306eaf87a57a52073185f5678fa34cd037b6fa8ff1b46aac26098cedd68c98ec245f39d46298d106a112d747d6ff3c187ed3f383e234afde4d69f0a56680a589a2188d47643988e95a4b4617c31b26aa1e0e84d12099c2085f53c35e2479d81e90ba2ec7b62a324410c6dd11b59fec4ed66c1efd689c7a8b4c6ddaa19402e16fb1355639915323e65162d2b5eb51ebd26e36220f800c0465e97e34f96d941f4fe06133e452b669c91af283ad82717dea24f48d5a9830ae0fb4db8679cbadd01844003d660f4b586d154f71e919eaa3111b9d89db14e961091a879c404e5e9933609020c972eddef7e10192edde0daa1b95ad7d73bf38cc168bb84a04aac14fe28eed3f1ddb563b305fc19ac37e4ebd04202c6b446b0d5c87a787dffe926273e4061e16d6755dbe38c8a48a6eac33df60b7a2f8670d487b60b64225e9a94292348c41e8111b89b3e15931d9c6ea01d44e9fdfb38d3619463f2158f73baa6d46eff35114bae59179fa7382b872a277c395055fd79a97b7ff4c73cd9cbb44f35a67b324bc1fda1c6803bbe094667afba8e94d95c90c1102141e56230ecdca0cf48b2a71f90f6c606ed4e8d099d870f74a5d5d29b1e88e491185f10ef4bb4341ecccffe5891128a8bc0c24b824cc49e6a8684513b023b477e9b36f9e51c652e489b5422c7accf375c73019490c13528e2860cfb21e4fe849ea17371b5e6b5108a60fc4e76d24733340290305e7449ce1b18634b9b2b4c7c77bfdd9760bfd3225bca6793f172f87811952280e50e9c39aef00fcaea7d33f9fcc3ec71a0eed1c882df071ef3b62317efef41b588dd7b14cc2b9d9db51ed133adfe3d9a2bb7a58e2b1950b67ceae9e9d923afb2067a2a8fe01138aac224d3dcfe36446efd270f7f25f5704a2a6ac6ef4d5a4bd9976c8e4a30437f320c0de5ad81390e2f5302d32f434e38537793368012d5060203a2db6e01a9b60dfd4f7f715cf94858a6d906904081bbe527dfe46c63138eb7f264e723f7261ed66bbfc54ada80f7fe44f96523a52a944ea20ea937160ef1f81a791f808dd9566c96d3ce02a509a89913f8edadcd793d3748a41872e664e9567f75a9a9cfcf855b646559a5d21b125c897dc77a39bb76ffc0711bbc38265f66cf046223437cc2a785e597d39dfbcff389face02c0b80a2e4d6ee8624afa54b6048054042bd602a5b56180ad76a3c4ceb33e9116f748decd198b1e3b95da908820d70960727c2b944a9d3cfa7f28ad3cb2c50ab09051f31e505246d3cfd0419ed7a751e77ccb0eb1a10805a00293d7a3c2c8eeb185650c4e6a53be509884359dcd302a9f934007f56fa77a0df34fb6508e23a143b00ecd1c5dfe785a614e8254ef1f0507d98d506023963208dd92f2fc601ea450e0ef887d9ca577891757166be7f8e588a79243682bdd45b1105193241a70d2c80e306f79e23da911dbf228847ed2ab5201a04be8ee9e88d0fdbc7a8fa1aa91400a93225dafdb0baa5696d9fb206dbb4b92d5b80d222acb4fd66e530a05bc9bb63393e8aa1ebb5e707726176a4a955bfbd35d9b9cd3a7b4f8f9127fd59b326e52f16f23efb5e45067ee7c434e372bb3cd2bb54840e7c4bda1dcc175c802d7a41678fd86355f8e317601ba49d7726029d96f2521bd1e1fe0d640838414e8af1831e818dcdecc22eb5cf096ba43373ca85b9cf9496e75bc5d25460b74f3ceac87635d2d84805f77edda701a9274be1fadf683fa9d3186dde6a35f02fa3f7fc323de87410273852f504355da7964060107448edf8fb4a50d50d25611f1a23209359aa588c15dbdb3dfd3b61bf491d718e9facd9141861a5d484bde9f377a4ac5572d7ecba04b3116074052dbef6c3f7ca6d7a11f05278740d3f333135522eb8bdb8017ba25309ce3908c21cc13e249d73568a33215ae4b9b9174365d6fd072c1a888a7f5bdcf8d1c9f259c243e18d7f7ab2c627b48df97200a40c052bb057eaa9c9a50548804eebc4415e8e1cadf4e6782cddffdc02299c6c11f0f9bd3547bb2bbb7e81fe2f9ab8203a83cef57adbdf42aaa1411e232d5010da9e3e2cba33ced7bb8ff9018ed1c82daccd06869bdb6b2fffe19b3f90b7eb04f20a738b7e085685b5c7eb00aec51ef4d8c00efb0194d26e79a429d302ed759c4ea3d432b3feedecdb2d336e2195eb0c0b2c44d6a0d177a42709327e6ce73a6318b91b8b7679ef9c13b0fc6b616df6f11f56ebb50a8c3195dd6b73a3f4c3755de12bd19a930cf39355fb941e55793170054a1a2d897e57408fb0f4815c0bc6c5fcb9504de72a5f4a8f1c4c7fbd2cbf2c9fe1a4e99c7e6ec4143b985c89649f6427e4b53b690572c8b2b9753f842bd8663bd3593d9bc984b6473cc9c7c6e69288dc77f1ba782d6296f8125d5f913ed6d4a00baf364cebca0e4ecc6e399a2649f02bf235ab12f24c456ef8acd2e2c3b753b504aff3dfef1cb01b29ca27f791006756956442361d615fd21aa1c9f1ddba653e25181f3e09823bad5389b0990b557dae2072e2e5aef53efa59530f1eaed1928cbd8adad6a93e1612aca093db81891230c7ca07c41e53990a5e6dd16e52be45e0520f82996ae05e4445ee9b52ed28bd3bbca3aace39f5116138870dd6ecd81b365a2906586d1d9ed18c23bffd2fb7d8ced5039854e880d111257d4e3381efa10a2504380904dafefcd9a90660fb6ec2106c411a2187bc276968f5848f896fa9d8d6733c96b78fcca0e50847d3fdb3834c72a60dbab7982864764f02d46ef7fdf41085c49061c884f57fdedf877f400c8d590b62e06d26f0b96c4ea95d97b25f841736e9802da2abb26067418a9893373f8a1e3122c2577c607b537cebe60cf1e550cdb0813efb538788d6697d6f5643f5a76c3bc51611b650b3d99e613a156ea98074aa5fd12eeb2b21d187b6cccffe5b41ccc7e39f30723929ce9bbd82c237f8382f703f9a89986b686869798e2166c043e2730cb6814a4f5aa7be85fb7978696d137d8cad781ffb8c71b81092e8158e7724a022c96506707a7ee95dc3eb3d61ed0dd963a75e1a1810519ce9b1b2a45e7bde232408d200c9365267b374d3f83343351b8affadf9a4f06d65183012be50193831a02fd6cd1d0a6b241cd58b02210b0462e69dcf6dfb57af58603d76f76de0a9bca923df9619fe32227640950ee811bf151819005461a2e9725f20fc84345dfc1cde916d02acf7ab5ca014a17ca4190653d6210cc94cfdd6a2559394fb06b7c17a46d759d90b4ecab6066db5f49afc51c7744d646e9e5d10e1893b0681bba8de529fd4aec9d872b5c3fb0d98cf79b8f8fac4717b7b9b26c98768d9d42f683042f707922a5eab08d0a91c474304b96c746577d40e24af19514ac2bd647c26cc1528d3e31178ea13fa5efce99b75075ed95f502f092d9a3d5e01adae8c242360d53225907843e497213deab5715d352fa1383845c6593a8ea83a42edb54310d54a665bfa04fa7e850b4c962ac62d6a5399338f67dfbded86a46c57bfbf89787f3535d0857414b4a9bb45bc51ea82144941ab2f5d14b77596229239e293c41f3c87da1f45a774a5631f1efb4d8149be3f83c5f3f5518b4b850d49a83f06d5abe3ff93d4cae6cfd4d869670aa74689b2d7ee8e57a5b8b51168206b15f806d64095569c8043e0defcc90a11fe77718a13665709b85f5c36f1ca7b1f424686c667bdaa8ff2cc755a39e7cc576b86f64ba4df38bb6b6f713d60f644190dd133d24066391ac97afc44f3c713b980a372fb661dc2ebd018af935ce72c81e27cf86cfa0de49fe9956d747fa063ea6ccd3b159f0e28be65dbd669ad1b4abe0c5adaa431de76457773c554c68db3778aa20b35818164a3586c99e6c95e584bf0b227ac622a3c9388084a8effcc6ed600f88d87b00bf1cd704d5ae818dbfc27234a2c422bac6eb2749f864e33347a0d3dcfbc16fc55c9bbbcf3c6a5c6532fe00c700c55fba33b44b102eb7cd9609323c8b71c2e6ac9014a7468ab01c0044b2bf0a67c04adc7a1da72bba94d82bf8277204ddab9351384023cf1b29266a1ec2fce10a6f2e8f7097e031c72bfaed89da5d7f7255a5bbe05ee974317dac29700823254676962c67571e5eb9e85d8fc5c7eabe0224eba064eec0315f11e842c2696f32c0bc26b700857ddcac50025e0a726d5d9e0c9ba8415982e1f553bcdc86cb9bfa7cde7dd7c5cd5e7a145e9358fbaaad1f75ddbd389a9468df3d8fabf163c9a8c14954bfa967b8afa4942a1581e9e27fb90a24a546143ade5fe397cc62cd18460998db2ac523bdba23391144d19d20033b0fd4b60b299926d2f1d148e579348b4ccf47ecf7bb9158ba21abcecf736d6db704ed07d08c9fa896734b93b066ba925f72254b74181dfdfddd0002e5d4eebe4ea802edfd4a567aa1bc3f2bad62426d1fb620157407dbe6975dba25d1ada64c17c0a959be43103ac82085d89cbb805f031532aead842c3eeca150a76bfdf096bc697a3058461974eb292117fadc32e71057ed02c7c8ee5e7aa169458b038746de2eaed0a5cbd87c648f7328529b867cc4c24cc523d8cc4e00764e89a3c605b4c24af2227b2653866132940b22206c566647900f23757906d00dedcf10c766fa72f20084a99caf1edfa6bc5432a1bbde4120354c7c78149593a038283680289f185c291f48c5b6223eb9b4c6f7675e4462478cb29ed423ff193349802cd8e27f9b10e5795fef3757b669aa04ef5a367550014779617e139261cfa3e346082a7459be3af5d2923cfe73f77f850bb0dbf6c82a81ad104dcc00634f0a6ab346c55056008efd16cb33dbc87ab85934de1069d1b6b11596448a451d592cd88e4d8f6d442d8e18a4e6e3f4351c82d6688458aefc04efa6598f1fbad3c435ffb2c446caac4a7a74b6e8f7095909d4c940ea99df8b164d4102f0f7636c768859d5cf5d88cf558988399eb5b12039dfe5fc57741f1e37c3383c5d8cc553a86edff638e8672a322a591897b23ca95e93aa584733c507fd75ac1a35bd014cd35dba8bd59a9216d86172bd5a44ab51913993d85908346a13c90263ba691f7dbcc17cd85ba07bd8093ac92e012789b581aa471ef42a856d769568afdbf0d454402bd81bae35b26f87e0841e77fc18ea9a06919395dd995d4d277fbe19d37a597b8ef368f2653d56654f15ecad8a6896a3208a1157aa4c159fbbeae9baff8997104fed1d3170b3f11d8432fc65d2e14d6f9b0722d321e8876cc854f24baa27614904cd07d81712a922d3a1f77b3e9191f5515d74b988a195640c4c416ecf4113b218e3470c99744a249ae6afef6b6962291712f20a13677f1fe7a4a8ed09a706b31f89684f9fb589a07213c2b00ba46b1f23e696eb6bf5e9d4680a2121dfa04812a89ec8442f2dcbdb9ee351e55ab65967b947ba170b6315316c5d759adf09b33d0f1e6e6db1a08b33708ca62ca9c086e5f48d0062cdd20cb4c578aa9867fca980015365dddbfdca90e26a6cb75fa7fb927f9a9b04d6036e3b5dfb6ab188f59b106a6060689f483a085ea6f195698ed6698f083c7d6dc2ebc92f61febf823256a14b70434ee37afe2f2151edba1a7ab750e97622ae7b68c12fcaeb093393c843cba5a8496aad0b6257d3e5afd49bb9ebdf18435397f0e104072c15002633bfa6d45fee9ed76e57c6f0d1a8f57b2bf176d8a964fe28dd2aa34619a8367f901ab3280763662108bf837c8e59f65e52b12d3040131cb9c78c51b73cd4cc725f5dda1c7f2db13f415cbe5638ef1069b5554987b3ca693330a35431ca0ba4e0f162d577b32e3a0d222c73f3f95353dea19515ed6d729c4571df7f5a1895b3a08b4300e9aad0512fd7736378b52ea95aa4ae94a7e71826ed8676a1f160db95411a1618fa92b859373ba546177fe473b8f29f3f9446bde0bbfebf4286f330c775e49a0fcddf24954e799215c815662e197ff279ed2abebd803e15edfc309f60d6381c674ca926bf89bf5f00ce7885c69e5c687b839d0e3c68e1e1959cbf7942e1a51a2bf3384751a322fb3d6f8080fc617c93279263851f8ced97cb07ee38d34d1c50f56e12d37229f35ce4cb23f957335503c5c7d22f3172faaa06f70fc28b454477e3af1fd32e33e0345745e78397bc877beb7f758118f919fdf5289fd7c28e7907b537656422ec4f09e3f816bc965b88945779361544a0b7a924ac8d83141679d4c8aee58f30a3309d6b9312a8f91f95b41fa90d7ffc92e37872440656c4a94f46a21263a5e7b2debe2fa0b7e96f5188bdf0e58fcbe47b95e478d6a635f221c1f9922b4714fb6bda91bb7ef9ca7db3f362f36f3e87ca9b3e95beddf4513c316ba06b214c47f86a200fae807e42c10ae5b0b7df3c9666ea9f83d25b11d5913d5dae8183f57dcbc173dc1c1b42a9aa17bd665a9a75e6263340188346cc5b82c36785344558b0593168d6f3af25c775223989f7626a82f7106789103983c374971ff56abf513a4e5731f0eb6696d828188feced6d1d2aed53d8f92162b624f0d50789263621cbb603ed576aad5641a838aefa407825cd4c40fdcddd719ff3568adb3f472b6088ebd5dda8915e29fce663909773eac7e5a948e2756c73539d74a553501bf170d8716e894249b3f9a4e40265e092c32ff3c253e7fcabac9f90d9369c6e62cdea543079600ffbd2f7319c419c1ae36453a43e97a4b403086c27293abd23c5dfd7f2607b7690b9c68e85ca25019ee11cedd6c86ebac25f569523534f8fde47982af2bd90db094fbde3eeccdc63c62b6f5e41127d2955c36ba2e2a06cd86f13d18249dedf289defc39b1998e6b65e15ce62090188e935cbe8f936754a602e963ac443ed14e71127be054b5fc7f063116875272cdcf667e8e412cb25324b19eafae25a4dc5be551e5dade539458fd69f7142e876a26856b9d3e4012434a724ffef91c4af91de7eace3241aaeeab72f938398acfd155e6c78f4c3eaddb287db22ee7405b8f5b2ea324758eb16d06390efdd3d448d163e8d2be658e4437d73d31732743968eae7d0d2e0a51275b0038ef9f498de327ba9f30090f17e8dbf45deeb9022ff58fabf3973fb4b9346cd7975be1461b95d4267c981f4ce05411549dd8e1d326b4c29776e89b010caa44e47d1a4e05c5136677553d7f683fae5ce9abd3248a094d72d5bc1cebb3a7e0068c67d41bfec53811ebcaae82f4e23aca0d942387dadcbb38a99637728ddd5c744a41af897f70fa6fb3b2334babd24a9e649eb21edd914018bb4733cdd85ff82f872c199eca0245225a8091862fc73a2ec71dddfb6a50f46f0ee863ea1ab5a2cb00640f5c3412d65b3ed20377ff5192143b99e894391059e2a8deb9544a058d91c78965180b8f603d572570b9122cb930e6d300d59fe0d8fd7b0c44096536e8a45b8263dae8842bc19601d69467dd58280abe76197563d74bb39ca3fcc6e94652818e6e37f9a97000ce9a08f3b9514519f8e321dc25495a5b10591a70a97aeae58a331ba64f7cb015ef8236857ef1d82e2bd76705cf652695c30a217a714943c70460f880a365da269d63f2253e287cb4a8431840dc5d53f3af867554f437b03d08051c9c34266b662c237b620bff212b19f250d8a8b850d7b8fd0c9eefb12e640cc72a29d154cf3fb8beab63a8c16d68c0cd0b5aab31c1426615ed7bb3d3f17bca40322907d562e2bf89f1dfccb9aab643c2f7564496e28c278cfb48e29ac11e820e7da0235e1f52a807104c7c0cc5092c28ef0aa98f8e753c9e5ec93f717ffcdec8168b6a54169cf627de982fc528ab781ae2e0dbfa63dcb623bce09441bc2e88cb3d1cd78270a0dc399b6746e50d8525d64a23c71bb3ab65957c143c98051f80a9583c872df7b1b6cf62c432bbfb1a8f862914ee12b6a8ac7c9d68fd953caab3cf6b8906ad65e00307b32c1ce7e7537bbfab325e7895aeb0f38a43a87cc1ed6525044f7513eca332df368575d217c52b0a6c5827386d3cd3d190eb3cb4f4df5fbd128d712b7f82948c2c73ec72e1671d3d4fc86f6c57f01caf4d39e34e1c32f3619f6ae1b18b055cc9639d2a44b461ffc79baa5bb8abbd6ae70f926d8195d64df9cc86f1363b14ab378b3090c5dbe03f4bd86d08385ce25b1332fe7ddd86fa01c92a50aabf126ce23f1bdb21f57099fda09dd8ea7a3afb62cc5ed76d07d5175772d36597f12c4dfbdc1f2b76f78ed2c779a86a51197123990b0bb0b7ce20cca477ca3b489bab62771282d8a09887ffafcdb2706267533a22fff2ee4556e1c3b5989a8fe43b01e1e0441f6d8d455f5d21390655be53deb1e2a4310d76eb446e88a35bbe686efd16e34e938e1e8b3cf16806f8ce9da02a129825682f651f7ee97693d51ad95f44cf6d534f762c8a4724733ff2a8bc32d849981a461beac304f32badbf91b3799dc4b155c183f6224b5f5139273b86c729bb0eaf98986447f5b1effa4a67146a18f861a5f5cc12708f12f1f9a8672e28b27911eac832630c368b04f2ea0ecd2f65137cd41ec1373a932aef98902319af90b24f17604274704de509c923495a38e3654be2a3afe7f93d4f779babe8f2fd74f9de0ed01f3df8d1f4fb62ca95d912e80b2f4f3db273f4eb324c128cb99df674e171215aee5800bff69569fc6fb2cea14e9f1bf6dad0ec36d69b86c4db4d80c33ff4ee6d1c90d6316f239c145d580fc93775c1534354ad909c08baf515811e00cc165179dad800d5a1ee4c9ccf94bf176cbb9b0e221e49c48709e472170554fbea26cbe7f7570b211050678e5c9f8ab5634446590f7ecd81d66999a87ea7da6746af1aaf7cf233a4e12ce6fb00f29bf9781077556c7b5e10c684a669903fd74ad89e7a8f99f4fafeaaffcb396813e897669dc800a17ee5db8a83e2dbc4779c93189208cb769503b7d015a365109fcd42810cc779245d5a56d84a8e98f8db05d7f202660644b897477941614bf2b37d6f44bda71cfdf05fe26f2763563c033e3dfecd33086c84b441f61b9e0a591751fc6506457c9a3143b5ecd714531f302f623a8a455a7b6f7e85be3c6c0770046f0bf0247473bbc682b209a4590fd66ad13e2be2b6adb0e854dd565dcaefce0636242ec3ec9cb5134b33db20f375868dd0fbe0c1167c0e6bc014cb76f28b5c55c228a7d94775bc44a074cf4b1cdf1abaaf56311f9403333b01dca1877a399291710c34a6edd6b684b350941ab8ef8de0c5d33287bf4dd4f9fbae1f0d1daea44f5dfd0e45d583bebd2c644deba31eeed06a3a488f16bec0ba2b74c3a6ffda66e754e717c5e1d49a26434e552f02ab78e4e6e9200730793c20a5c9a536924954a0ac379ba07fb9f6a4742e81514835780cd4ad7a1a6f0753bf109f8215dee0c09ab400adfd8bc509f2e230446fcf9cab835872d2c2e79c5185ace96a057909c46f078f25782b4a42c6cacd4c37824bf6d146b781b12d422369522e759283f2540e3b53dd0c6c09647c5a4043cd1bfaa176df57a85f3de5ce9779c0dac7808d3d9d1561e0ce9e017bb3dea7bc8017260c21f1499615e324c66b4738e7cb0b6072bad0cf175e1ab14b27e96688678cf368c9ae0ebce61d3557fa0de1022cc0a9502e64f8433edcccd88e5a67181f381ada850063b76448354ec599a4850af4cc6df1c062fafe32b912960a517b11fdc7d1ba8d8a26a9032946019e761f2e1e15a6a30c8aa8d0c14eea3650bb6fd836a434541ca26039ac5eaee83d555030f89946d552ae75b089fdba12830aef6d3b3fa015311a93635e9b4a6ffa985193783c79554a1cc900ca6bfac0f31e129a3d951312a1ca49e03882b7025c4532187b9121670ef4be3ce21f024e7e5bfc46f83fbc97714b2d4f590a1ff0213c389538ca3bbee3b419c7756b52d1d7f6727a7dc68658a68a968c532595c99a266b02364fab1dbffae18c8e372d08c76393d56414f34792646b2ed32cb5cb46509f257b6135215506eab471c7d2c8d97b30e7e3d90c813ebf38a1b3fa5e67a948a2308f46cbb090e50fe2232e58feb1fdf7cf4e8e12f8006494f82656fa5e3a9c26cd92831309210be64895cef72b3472694af2a8f21bd32d697f7faa8c0d53eabc027d4f0e4bb6b42a35bceffc080f606b4127b017a90e21ff311a77b0de2b1d95d124434bf1db9bfcc24907a92022b111806a8e392d92024de678b346f86d8f5081b94cd0442be0401d281a663a316cfd1ec74bb3532d94ea365721b3cf3e7ff17102bbcf28ed79778cc67a82d1af204c2fd860cba0443d18b76e85c202dfe8fae01a60fda98d428ba44e4233e83b91a4e9e95577b37d395e0c72f2860930e42d8aad498d58843770fd2d688b7906f50a54c08aff7f76de6286d98e4e827492fea38451e066eb9b0370457d5b23cb3afb61d0a1c39aeb119bd751ff1c9bdc81bba763a954502436cf440ebb7fbe0112887603dcdafb4d18bdb3b420ac2a9bfdf79eec6e5d1573988b009a28cb0cdae24f00a8338600720b8ca4f58cf9e8478dcc186d9f2cfb1305f8254d4c969cbc0c838f0f7b276fabe9b4e8b1ab6df237b22588229059c991ac06f80100cfc9b8da6d493ad7e7bc33179e889f748ac2cd82c227fa14416e51f95e89815734a2a455192d7dfbf33f401b75c2710bf44d3937a51b020a8d4545b7cc61ab82471d930c0a4dc68cf5fb4b22f0c0a290a42b510e395972e0955b4f665cd3f7e49500766a66853249a8cb761fe8a6d752644c8efff6b5bd97c52db4788fb7ef8016ebf562a4df3c377b446c60b9ae0da0894ae165044526304b95bd6d5ad8bb0f09f91f0eb250e4cc33dce466728f1c52786b08c5aba118ec557a1e460e8f310a9df1c9443ffbebf8715352f2e397f8c3b90fdc73affc6ff573a33c40c2a9e53e041f668b0f5c2db30b58cf4275495ef08013ca4d9b9650193541331ae8620eb8ca6fbd36eca91487e1465e499948150af0741727bac3163feab2e7a8cb8b60e562d4490fd67da3404c492509a49580ebb765f533cd4493f8053a53ce88eff06c3c393ad7169db0148adcb35cfeba15b9abdfac22738d8118cc3eb2e7b8b7cf2548e760409e88b455db97a5ec8dd9b780f6a2ac3d83bc54ed881f48fae1b7bbcfb305bbd646cb53f7c637acfbff60175f5a1cea5be23595fc58e92d76a9008ac4bff1534d26688dd27b1a07950e6471df15a10ad0df6737c7164916005645ba0582338a9a35dfe6c4eda83b3ea82116319e574b001a1a081c658585b710762c4efc3ca23c11ecfe8988f361d57fff130f06d6360768db8ce97082bb7cd8d93691d900cb590e93224d06895ea65e2f9ee551e206f8722f2a9c4ceac7f8bed846f4934d376c114472e17362fad3be3f5f57e38789f17d7b4793948018df0141b317c86e632297ba66f6e9d476217a3377d55d8261dddc880c69b2d97c6d013179707269a1014d1f65ae162a4bb43091152f1265ea68c09928ebe7cb06d812d16897854933cc20c71d66eab1c16b9031a519afc4c08e3bccb5e4bebb23baa87dbf4d6442215d42ca91b1e13ddfae4f79d80b72eb82d83032c6ff51674d615a8ee10805b3ce2184b33b999be601f47b447528a82d72bf8697fdab3b52ffe112b15e3ba5f33f0f65887b042690ce8aefa2e8a865f81a91836af4e2a9e163719273b1023d8831366f0e2ee7c9688fbb30d11a68bebd433b008daffecc9d9b9918c453f1cc0ce5fc0137303307e72465ccadfad0045be8772f2f17326057e75d9eced480dbdde9de1f6e113d7da887cc258cb65c6af61a1025c8c4cd4eb6ab37974b6cd3f73bc6f7b946df5266737e04bdf5850006fca065dd7fe0c408274571ac39db4b3abbaedf91d67e96f0dc59a855281c18ff6a1fa194835b84bd5c84e41f6a6e34c861f5abd9cd385bfbe1ab77f7c37a2398099dedbc31dad290f4ecc3e7645a8588a1ba4c82bd398cbae8be27c3d70e87cf2afd8f2adc9cfae0c921250c044a685f41de1a07810f6eca123c3cf17d947af55ac238b2257233c39b67737415e2afa7d42031613e97af0e0fef67aa5ccedf3ee748a52b6143480f704b06134196adc7362beac84ab0f5b9e5e6dcf150b790111e826a252c3afa61ff4094e03ebdc390ef057020b5035f05186a4b476692a8d06c0c5ffab672de1b9fb9d3f202af5d748c35dc7e999c4f2b9ee8bdc6b55d4f92e68280fc656664606b6c94d109629ba1a97c1512c427c6497325089c70834a55b36cf62697c1741361a4b2a1872ab63ff76fcb0f8c044b2d34789f2699c0c2cd418ecd95f0f7f5bd06d71ee563228e619bcbf09493e6e61f0b7f062129094136a05d751483bd63f60f75f272df61b9a828dd5c03d84229754e1a80738645a78a3dd86e53d4def86ebb37c584825994c3ae7c03b057539c2384c6f30a9db30470aad6a4d4cff81a0e36e3ead0925ea2a6996b2230493515c876c36e3d2d6976d4bfb7a6dca00a71c7effc2fd20823c719eb3f2afe31df680ec72d124a2a22360821c171ad5b6078b00d5143d982661a825d4a48af028324ca9159971f04ec140f775eb723560cf3eb872e630048f1f2b54648e899359cfa2821158d99b3619e5f4c98b857dcff8328356e48e185a6c04a21fa9f8cddf0a0619cd2aa49df61969c524625037492d5d15b133a779d77615746e7eb056155f1e397e523cb32c88943c2b36074750729b2f210f303770ffa636d185eccd40c5303935f8d74366989e0859984ff4b2017a95e8ddd32f185cd7fdf2d884d96b1c25249e570342617b079853001ca24f5ce6fc318a926d179a6aee15bc09779d5743909672bd2d7733611b309f3aea3191163de71e207ba2644d912364cb744c7a29b3bc0a4595a16c605fb4cbd8adcb501af053957bb15955f004e103ded226779f5c51ed47413b43fb612d9c233b3b277e9fbb6eb25e6ecd6cbcd8d7173b38a49e2410da3cfd70c05949411ed2ea66a78aaa5a2fb4f945135edbe38b4fec6473bfe63afd7796d01a8e09dae370e5de1e66508271bc132aa85ce5e77de4f7a0373c39c2bf06927764a7f629e254bf2e1327118168ac5a0f4f1b79e030afac024b787d37198eb5314714bd9aad65bb8aa521c77cb052031a6044a5abbd2783116170ee476f5a55f49a80d64ce0fa3bf6d0ec407cd94e624f7097fc086107465509bbe1a091e9f060a7c5ff73bd55627218a518c54be40c8b4a887c76aaa242c6741717fdaa3527b0a194bf14aa2d39cbdc665a84ac1ce6e64fd908b0af544e8bc910289d5aa2d168edf84e774d97eb36e77a7397c6da23c8fb0c18cb3695c1f90e18383ac39d5cccb190aabc6411caa3fa8723f233bde851ce9a382209eb3e4815db30cd967104ca1b0c98a2bacd3d38d67ed4e6fc03e066d3d2dc3824c01f363f4b18c6ecfebaa54c114bff0fa001038b4970032de6650f87f7c5bc23b7992cb97fd61c0ef00870406efa6a13ca477530fe3e640abb9a46754b5bb0728e145e5b4fb3f0897e64ae01b1379004250fc3bc915e27033f05d6963feffff8c3cfa5904d6dc225096aa82fe15e9dc19c453257846e80cb89848e7c31160bcef55fd28a7fc6d8d185dcbbcf583fa4f6ddefee1f4474dcc798ade6c1f021c01505a2ece5c170dfba3e106a567ffa4ea7fc0d4049e417842dbb3dbf31dd49e9f5eefdc99eb599bcc8877c13dc47d3387231ac96927c0a46355f90e0f700dff2baebaba79b7210c20fe214dbc5d21c8ec2ec67b9a076c073aeaed32485683e7a89807ff3d194561fbe0fd70b1256d36afade57d5751f889ea9a142a37175d6b518c42e8513fc42d9a8b7b3b3326fc24ec2081421134cc039485be43d167709cbb8d69fc42b59eeb8ed86d8531a18e14c091f795204df7edb53054bf3f6f87d117f1c738c40ea1bd745fdd79628e673e0fef0827f9cca6f21e105e89267c0abd65abf176e8b7cc231e620850fdc0ceb0e8263ddbb99509f17f22f4dd3cac455e8d05efb7780b5852ae7a01317746701d71de0d553aa09c855e60b2b6bbc4d1cbb19c1f6cead8454fed1c002fc35cd65a4b4114a261fc53f6aaca0260a4bbc9a6cc90b0ae04985f197aa99c9febeec528b163704229bc447bd9c7e4b43aca5f2904ffec51a91c2cb8375fd5a3df20ee2f17be5b8348c8ed73df91d8f195db92a86eac1ba7da8f9d440e78d5eba6df38a214db8ceece3bcca05e3ef65a6f8bd8263ff319fe86038e128e6f7fbb99ba260bc1d2f9a4856a5743c0f948aa51984b5788666461a32feeb65ae47102124021f74cd62b25106bb60e5fd0cd634a9b17488fbf028049d99bdcbafeb156e168816c5b7cf9d5c761d8dc55e3e6f4368aa457329c723b9e008017ac32d44cb8276b32fa328c5f5878fde376a0a46a5b567492b4d31c2dde829ec5d1b251a9ede360f36da71ef15b94707b8621b4f675418a5e80df6c6fffe43c27450b91bfbf503958be3bf7115a8979dffd66c49b8a09508f261ba86912b81cc82e676c056d0016b5413f3bee7c730951853ce31fba26aef5fbef4c16c732da2b4cae321b9390d6a338ce82961767abfafe8efa1fd68850159751152041e961a3325588f5cf88f6e13226848e87a758cdbffd6afdc541ea5774123a0ea0d938772df2546243622a447441f2343af7b73ed4deb46709003b2e31f800a5338fdb1a8120e8efb32980039263cb937d0b06ae8e24b2f075c04dcf279608199813b8b74a3e338220e91dae31a69f27d46cd550c02d79ca3990b21ab4d25c68f54c02dec572688ddf0b485ad0851c6e41360f629818d20b4d7b9d87643bb8501804b1a32dbb498416a175f3d24decbb027234f86b2f02995496ed49004ae2c529e4368c2d3c3739ba04171af60ebca8859fa001cb3c6f2f299b7ce4e6ec724fdffe01173f2bca62e0108e4f6feee346764f554d81b4fb1a4a741944c48bd0506ae3dc2f6ce1a4771c5345c38804b776dea4bf64103ec24952aefb74d579e71776ce1cefcf6fa805dcb001b34455738481031e7beec8cd91060002ae0d8dee1f37388154d5ab0d641f2dad775bde415837b48bea67aa6de508f2f83e9c4d0fbbbed1a1090b1d4df6ef85271ec81f997738296b8fcf432aaac8dfbf0e5d3011de7cea96df092f0f8244ea7e09914d81c99d26af844287d939c4ded2af4f60f419469db5e43d09e59e706a0ccf794b1f05721ed9e8969dae4a479f8c8b094f919262aad6564875b412f923d14830bcb854c32ec046664037bea7646a559d1800dd5247c0daf291e426675d43f0b05fac8538ccd01cd17a440fb67b27867bedc3b9a8da756f659a98a6c0da2734bd1d4b8ada7dc7d76caeb7d8d5973f99298bbe471dc27063dc3226d54b0bca33d9d519d582585e5f0d52c399902ff30d89341eea48775498a19b0c0ffd2ba2b489625bf2964d2396349c57dbda64bbc981fc5be353cdf2274912be9ae0423261b5e7c9a6aba9b05eb6816798493186dc8a330dae548377456c58a8e77dbc276a9144a7793e05c9e6811468b2bed1f8b8ba404238c8fd8d5e855c587bf238e6af3d2845fcdfb7cc037078274cee0fac6c8e3f75f7bbd62960c2dcdebda49208e4eb5af57a24ead9fa355bafdbe6b2fdd17571cc4e76ffc62e760a4e2cf1d77f0d7cc664fcb5540a4d492acc18025e63a81b7416bde1d8c0afd1f4447547c5b29b6a5e7e9ea316d3ee210262d421535cbbcd0752ecc6ccfe5f1d3531911252a40c0f85988891d69864dccc387c8c6b40c246ee310519ae218786bd873b967eba9e9066deb2648e1cf6d3c77eebd340510f8ca0882f79f605b9049f1926e4c6d0e0341a640e6449ac8171808cfab3386491bff071c38553c758681b1892b5a828ead4f89e85d7d216df9df9474b8d119dc86c9507d18d2f75071ffff3f42e3c5c110e40d49235880b2330d4171859d533e66ea0c6e0f8cefa2ee4a1bb7b105c723c4323c54c64094caed5899fee3e05a73ac444a3b245cda7daa0991380e9a8275d5c236ee675561524b46ef22a4d96146da70b3545690bfe252e1c3f2bc204a1aeb052b5b54e10c50b8000b2cdd4ca2c6f303046748683ff856f3814e62bd369af86bb8efc1dfa538fd49b72cb2c04f5fca8c3719eaef45ce6e037385efe70e6bb0d697b7e3da5de2c9b1d039f1228d4f0bd7cc9347d1eae5176413a3001e4d2784b8dd4f276f454c3d722bbc349c2b51b521095ab70fd2e6f5f30dedfc464bd07f4a28fbf9ea2b666db67345a01f28518af4ba709a0dfebc1b01cbafcc3b53661682af6bba4a094cda3da2fe0c0e63019bd9c4d0acc09dd0eb6a00dd7f92c6caa4299a29ac60cd86212b324f51f9db149ad2990fc5e49d7e9cef3f9dcbe646ae012b926776557f40a1049debdf402ba59a8a2e5e471de855a6c1709fd7822c7b3c3767518b7d2647c783305950ad0c53e416c82dd9f823d22597432cf228f94a982ba3a89140822943e0f3049d102ffb62f71af66a861c60d4a1563cd253d4b3f07be0961c39b1039fca41b13cc1b5ea27c1f788ab9c4246648d46987083e359c016ca41ee579da53a2f811572d8441072e8290f1bce1c22afd6e5fe2d3b8ac1ddd44f81fe3a996001080acd79cd8c2f4a4dbcb7dd0522f7a125ed028c6b0ffd9f8d21a6d82bc7f1dc49d84cd8f49d2bcc016f9eddd35a052ffba42d666f7ad40f799a68e6b5afeb17a83579c5fb3773a118d81452cce3981c53435ec2aa986f3380fd918d66bca06bb7869ad6b95833aa82336ce91cab024265b3e9446da3cde7f4a6cc6e8ce4d0c676d3fdc9e1f75ac4ca6e70231a0dab793994b9a6afcc40951cfaf62af30b26c82fa9c5dba036dbbf2c07089c8e5a699398b59cc8f03578ea80005ead2e23c538cd4ded3a57fc3e7da3f6826345b1f057dd009700ac5f05ea2f150011a9736ba12fc363ad7712e537642ddba7ab1da7496b5c03702facb4f22e22272fd99c0d5eb396169460b6547cc1ea64d9b6b47c8c2d3d82ac73ebdc674c7c989e35bcaec468f15b82db122abf42aa3f89e254290e40775e3f536efbba235bb9a83508818835fded4a0debea24be6b9cee24805bb798cb4ee68d9b5204b8e546bf33b939992a75d54f710461bdc75fcbfcadde8ea4e722a8e5992f1d7b6d7caa444269c2989e197b7f3331158a11340dbfaf69376e0eb36147d392f956eeff66a804c2c25a0a34f7bb9d85673cbbcab7b09a6ca738cee0a64466ee94abdb13e9ae6d6e3c8e2b84e98d088eba7dcdd3425f57ae08e5993a99c87e5ad85beb025d0d515e76fb8b022927f6c855fab55726202653c66523dd8952029c52ec97b1ff39e5e14e8e24d75061d21d17cd38deee065e8409acdd02344f6707a49eb5e421f8061dd5a1ddeb34a18ca39eaad5b55079ca1b2074f09e94d25efba36543e32bc2704e76a1ce469e3f01c12b5c2db5c2a39b90da8eceba7a46998defad4160680e0cc90d97db118af4206e0f441d7512f6158335c54eb59a4339dbb9b9430d4bf3226b418fecddcd422207fa613e42031674f34c2117ded619d19cbe18148fc4b7298439c1a6c880c479850b8c57b15cea81e0505cc71601fbf4e97b11ad27a01745e4fea5eaa7e361271b56b9dd5ba7bd7ad4452b56e06a296bdb9c33140680fd8285a7929136767bdf9f893a56a0e92f8b314bbb8fbb405d639ebf3362c36ef3986e19cc3a4dcd67365854e01f596d534f41e64644ba153a2c078d5f1b8e2c3a8dd27e42feff5926dc5cc6bbf0c2fe2b59044907ee556c847823ef9b50bf6c980b1c4fea0d11c2ddab157570fb1b28a90c5135f48d9ff2c6b1a6e7c8e616212e1d0a22242da18b9dd366218e0ace7f72905b1b1d1cd97af4f87ce3a26732b73b2289a61808aae29734ea6e6372b802711e5f4d61fc0a291ee111a1ef6c37550b3e712877cf2da29843f69bb5f589456bff4dcf2dd654f8cd385026b1d2fab5b1813d42e950a438d9fa6aeee74a15b505722f34997f39b6542cc34b0f5518a3b43ab1866ba6b5384a4de950febeea6c2ee5e2d348c6d2bc7029d12d13fcbde9c28c100f1d912f0308302a7b8d343c9b753bbe3370c5d8f21f7980d6760f5138677288f3d5e01d7d0077bbb9c021fc0d80b16f83edbfaa16c22e8ccfe1c26bf12e2d4d493c97834f435c162f63121c3f7fc52fdf694d898e83ae2f42d2c830a5703b4b1a5fd150ed55f987482814371651e8f9fd89d5d2b4f06f9989fd4da77e72e949d815e1c8524eeee3b3c9c5f7b808b71fae002ec89c63b959f9f97543d03aaafe204493a8e8d3796e268739f8bad6f48cbe0287e17019c7c44d120de39b86e233cd01976c5a641f180a95cd654d58c73ef9232fa6ebebf5955697cc17d73f1b5fc8ee28d3bd18f2ae872a00f5df1fd3e5a2a3ecff100f498328a8223f7574135b9a4b2b7c5dcfd1cf7363170a3102296929e2e77cfccf3403fd1d931d87bae8a77a4c81572e5a17132ae9ececf17406f94d67ff2753f202faf9abdd1f039aa6bb67ba7ac6b7daa699d3e1051500a32f9646287d611f90fc1b877ce38504bfa902679b6c243e7f1481ddca604b7981514d4a0d71b898b8221f5cbad4b218dbea1d859c8cf070e3787d45933627f4348348c2d349403486c04dd6f137038b5e118d9f09064c2b395dde0de77852711c5160021e8491b3ff6ba3c4d4c309e35164cd657ec4eaeb61a7a6fffe8e1a24c6de348cc5539e9cbf71b7758bbbe31f93c4a4a127c43e3e9a44632e1518f71f3a30f5bbadb52c9075a084fa34995466bf2e508eb515ad871d9e72cfd37a54a6ee7980bfec6bf17942c3a77ea8ab4bcfd9c8d9be639d5e3dd49275c377fe401dc6949423f13f9fafa2eb41da351dad484c54dada434927f59ea60e9894249e4a2a2a39b3cd059442c16b7ba4d2dd4dfc64a92d0d875cd9317bbe3a64fd8db11acd3530aea14cdeca5ed756629c03d8b02ad48e8d62f3e21d5d51aa4e7b90f7230de6de64eb746e64bea9f8c68ccc6434d995b0fb4281ff901bc8f973c9304f73ff8ab86796118a085e1fc0a22421804f1336605577f9e1faa8a9d4a65e5fd4685917a8082dd9a6dc187f337cf6f8099596529b76885dc63c9b2ba71b24118512eb0ad4e9afa6ca02e4b69901f220100ad49e5820daafa91857b7871f61a7c93b3bb79f891a93ac22f6c4ef6362a735cce5aef170bf928aecd1d0b92e6bf4bb64f7535d2d5728bbb37be6998d83eed975ab925fcdac5d7259173afae91bf8a352e037fb88efe4325f784619842a5440a75ee2c4e920cfcfca119277d61413637db13429edf86b07f6aafbd3bc40c41f271482ea2b5752008a5442737f2a4940ff24018fadb946b9053b28df32bda511e46f2953db0e99d170ae1524b7082c995058ff1d9f6f2e38cc59ec79b0052278d8d7338b83f9071324e07cd4214c06c0aff2bdc072f15a93fe639276c9e7f56365d1ce4adb6fbf991ee0710457a25a8c36ae368f39bc1ad488c4620aba984ba849125585713a70226ccaee527b72a423608f16e23051dace50f8f83cb6279a41f26eed0e85f45b8c07ead89167c43d4479aa91936886f366c34761d6f2b5a081d463009c4b4f22d2d5a24e58fc1c20e89cf5933dec33f3797374cd13f2e0c251342e4728393bedaea0d3ade2b1cd17eee3cb5b220b41b76e283377bc8c5bc4bfab973d5145dbc47065cc3335312f373e12be8f4a7d2dc77412e1be18cc69c37cdec2a77ede410f3df5dc36762bc5c6cd466e530163eab7a5a12b34234964d606bed7899541dc2d09c8e881397c730fa5e53e92c8f8e5a5e4b3586e9c3a13fa31de5cb5d94640b7491872248fce2ec9ac85fe9af09f755de62e1f0b845fbda872187c181a31ecac7fbe300005e7ab0576726fb1f4e603d70e79316ebab70d6f11be14982a7ffe3af03292cc4d40da3bee3c7938f84567dac1d76454e07db6137c79bd1cf8ea2c4a8f0cbefb89b23bfc9daff29fb550749b4290c72658f66eef1087b28d3409c995b27559924bebfb42e9b96bb4e88858a85d4f2741a0fbb4295498161aa0da1e8e1cf1bf6d7f0eb43d6538cf4fc674d08750557a12ade604cfc87ba5fe08ccdb893f0976561742cf6499bed5903e278e3250b02f54cf7a34b1f86d406385f74f799b5e8ea56be5d040cfee9e7b1cd6ba0df679edc2045546c8d7c891340d26e3dac2ff97a738e446c74d198fd1af5f66b658115976987659341053d94a44717916035b886a2fcf55e42fcbcc38893f9b8d5d17450111c6bd639d83549d35145160db1c4e6da172d1498b81e5d072e96b756f32998b269bfa7cc049edca294b76c1c24a40e7e33c56bd9d8c8dbf75e4d9f28c276e771ba7463dd656d792d48e10992741d04539e59ba2b36daafe8d765dbfea46b036e3a3e280ab210bb69ad34bb48f1cc864fcf7b117765f8df71b6e574ce26a6c6d36eaf706ec52d15bea98877d5d993d6ed4bb1d3c2fdf2ca8c46031edca93f99c76a2395985098b1722d5cd836951dbcaba0b2a400aba7672d1ed85a98be5621cc5b9615ed3388533fbe6b6bf7f0e4801131c8c8eaa04935f492b68d0afa42d3399f2516f30033f97f7a028281b8620630c7b651590f262d17e65ba53f096a4483320f06ca2902c730bcab019b38304bd3e1ad8da52afedda33a1433fbfa35e4cc241f98bdca5b85e42bed0afcdc8e5bdc5d4c4a37c4a40ac795d53cdb557347cb14a4963e5602f28c47b66498bc8540f6a6248e02dbc4aeff4cfcdfb553c449c054172d395d7be7682b3579657a220c020b3bf69bca32ff5b6bd8aa1cbbd50110899b0b8fc5f080c3ed901c6931d5917d8418e52641ca45f8cae636173e4da1d6f92ea68e1e1e9692760b49ede7342ae71519412acbfe161d164ed38bdb5ad7b6f79890c8714ab1e276d13f9fdc7e7f2d4cf05316ade75167139e32a66d408ecde7d59be5388c5d6a70ad2114558b844111b0f1a1d2c88b75c2a0ddb834a080f73cfb973428ee7b84b51d93a68c29a995ffcedd32dfb6d10e7e90166ae6eb83c938bc64fa936d547bd022911690c7fd8ff110570b169a0479730114140851db7c3062dabbe6a7b7ba1918f889eccb66c32659b42b8fbd68d40a15e7cf33c1bf2a1b788f94f6955d173b665196ae448193d5662db7d74adcce4cbdbe648bb980319104608c04386a464df06970f7fcea91cf3328632e902b428204e3226b8e1329828a9473cba9bc1a3a669f5c025cdc4500598660d51ebae70dce9374737c31bcfd9f5f8d8b0c55d660b14f9ebe3553b1d193dbf246d148081fc5921809ac216ca59282724435826c0d9424b831dc385297349246ae8654e86a151fc88f23d6596f4b803b70aff99ae6c92cf3ecf6510310236215b4414a2a11b14f7cd25a2fa1b666f4b185c84dc1b7d92d855aeea21fb04dfffbba4e5b3e05f1ae85136296a2971c94cee49adf7b807968690142dc0fef1b980b1d9a1d8766bbb6a0829212bf20b5b83149833f1ca5f151b0edfd414e42f1e7df3cdc771aead34c40761e7a6b57c3a0b7fa9d6775f5c66ff8f46c974fc47ee0ce4e4cd54d95a2ddf6b7285795dde2306dc51e9cb39f46a20ca18c2ef7fda66c534a181ae68e95f66de5b5dee50ecc734280adbd49d3d724a200e8d6aa1bbee8f5f324ef2c2b537c496d2b836a3fdd77a6ae6a5c54fbc785ef1d6bdd0a06f31edad570173aaa666ce2e7a822213d1f93dd0a677891493e01707aa987bdc73cae2dce8329729e0a3491f239b8bcc9ef6bc27be4405070ce873aa8637df56c64742828d3a9c91681d35bf34bea000dd248edffeefc8360a6b1867052e055e2315b5d7122734b572754dc603f8b8fdf5a15258ef746633cc0d5e46ac74d2bc4ee4d20a21ffb1b309c3e0b9863eb79e509d21d5f02da89ca78612f6695fe52d3796547ba7c8a69e65991094fbbbf2052035719148d8168a525d76b08f05cd4308bd11f5ac5e0a9085643eb55b8bbd6ffb5acba56684ac3b32164a7ff6bd66d1bc23fe205262cb39389a8b2c0dc430d1cb4e6dc0e94d8fb7226cc0d5dcab5aa0d3a7f910c451159d32118296657337130df6d684ad574c3a29a23ab56f3fcb52cff8ab098ccbd5333ae0862812362763143903ded699493a7fc10ed811513f612902d63a4826700b59635c0b8be4f9fc2934b7b1f226f4bf688348c20f00c7e2ee5263182ad27b146562edcbe707eb33dad6c66e3e40bef2ecd420f2a839bc7ab5b79e712a1d554e31b7a3970c130a0550a99a976f1f2edeac9d499ce7b855487d2298c1c69c1b91bb65d38edd06345be804d04487a9731c9cf9ca2c9c8e15640988fe81a7ab13e3b7d96367988958a7877ce640e20fb2f03dced56785a5174a99f1fe5ed682185437465c36cf54bd889d2f704cfcc11ac732f029a1922b532323f01774d38b1243cc589db99ae80f147a4bae1809a040f9566208535507d74d7e42569725deb11fd9f11f6461855789af804357db5351c0fdc6b361de3765f9c23c8e2eb3e3ec5175f0556e1de6a46a1e19a14d198c76d080b17213c6239aedc60919852e9a9dc9756f916018b20e27a9fd4a6436c768120046d07773e55662bff8fff92650b2173e9649f743dc1960d14be58f6f42bc0e0d45f1dea0edb15483827e413146c36d1758f53e7cb1b57d1f748314d5d6f81050ca7ac7249de784d5055e62ea5fa0e10287e9ae9de9d1f5595246b243523ee1529b18fec005b359831f938f7d4564119d899b79cd657c13403b432d45faa141a058abc83e49df19c5ef3f663ac8b7d2900a1c67241d480793c490fdb2f45526f738125efd0c6c0ac1f80998df9a1b40c50c8aa496fb9cdcb548e0addbf7900cb8f1785751ba37ee75c986df67b4f7dcdfccfc8eab504f631ea92f916bf2a4e7f025c6d11b4aaa385e0a22d749a04ba0107b96ed3c174a95d2bdb9b453e4032d5337702604d4a7f35d1fed7c26e0370633f82e31c63fe30bd1893cd53f3b475f285a9f903db952b5f22974bf2fa2bb0a9ae3af2b4f789f7e984c8cbbf619889666c371cf0bbdd4c9382a830e46951dfd648179e040d43efc905bcdf60fe731ae20a190ea1d3a182346ccd841559e3fb00aef911c04675daa5e810bf17f8f8a249d787525be61c669c499e51f29d07b5aa06429bf137e98862c5ba0e911402ce4e0e07302db6e98f0afdf41a5687ef7cc60ed57410be055c68fdb2efac272fc468728906a2f798d7fefed612bcc3e09f5a6afe16fb5be3da6cc25ae0fb6ce51be4a3f8b7aeb6e0c3c0282c65c127e5ab662e2d39b36cb9c1a2d1177ddcf023bac5ed57969fed9cb8e60e3dfcbb797b337ab232dc6d2f38d086d9ab34b6c7cd933e5b2a7fe3457c05c17bb1cc1b1477f18fa21bd139aa79af2493d9c1b49ec8bf119fe9f6f32d6e5c5720c90cf7c2ef0a3b19d972b62accfc14f746e400fe583b6c94f8a34cb364816e00c89b13a607b749d2d2fdc82c1345a850a1e49086b1cf74bdf0af78c9ce982af99ed1142a3b55a695111890e3f9902dc6bd5bb60fcda5fcfe47d22572818b16a689aed90c4277f99d18e6d775e2d9c1ec26bcf2aa42552956a0fe2f7485eb7ecc64068d7a0a73e87aaf1beeeca7d09e8675cbf4ed078fc613d983be7794897066d96aac264b1d75e86be9e6d70d09b978a7a403bd8707594b8c0008dc95ba194875345b62d36dc70f8dc25819ca949e533cb9b205ef27484bc93b31c6ce3be47af7ace1523359b22c6cedd3f7b7b9fa01a94dd0f368967ddc5dcf5e5854a1934a273ff12d2547d3888fcd887452e5b2973be055509e97f8c8d0099e4ff9e2ba79784a207c73b576cf30708ffee885e4ace240abd67f612589cdd980aca0add60278eed03636ee5c16a0bdc8184b3bc8ee5b0d999148660aa09cc7870a1ac5e08aaa28aab70ba1a486f8e1340552df333a201b7204d1da221c467e3c31a154f4608b975eb82182546af70a40e60965a26a82390dfe5acb3f5aca8eded7db68593953f2b03908176df43db0cabe3a0b90ab3b903b0a25ef1041970bbca84f5493228fae3a2c0fecc4c3458f75ad3958f5815d735569f46c96dc66680ad5341f9e3c1706e96de7791cf990e1c16b2cad4ec01182f72bc7a0baa398fabaefbbb3811f02a8e13c328afa26313559be85c2c0aee343a2bc9dc2370facbaf4a04eff57488f116296560350c41705725e4e0932c064f22c77a123a3fc81cd8bf97678b379a116f71311fd469e748acefbeb32a7bbdb5bbb778ec40555f8895b071b791bad39c00751cce646ce3731578b591d67f8e9a03326e4326fa483f43723b90bdaef4619be57e4d78275671c0bf3d1df22f4d64da77a1f13d81556d27be7926aabf436518227bf415a14522c1cd9f287960428079fa574f82a25774011f0002968eaba456d8419e38e5179bbed0957cbbd7bfcbc87fe1b8165524ac506416de0f3f5c01866339aa1bf828c08c94376a352320ff5a59636f33311d698b979608fb48e17e51445616c42c68b609fede872559e6d8f073864e16529cf2a2852cc61c7a94b92b1a034451d3498e5accb586180bd700a42966d4ec8443cb0118d3d8055eeef4211962bb47465e247683cd1e67e4ff1ff6bd99f7a246898979eca990a5a205017e37592613c97fedbdcd36c48644ffddc12819121a30f2ed9a1cc49f48d486f2bbb0da2abb9a79356cbd95a9c2246b1966738d8010cc71233d6643126178d93474f9d03f1afaea1d0609958d1f406e03d0810b0dc9ebb9b458123dbdf640188b25b00da80ac0441bea28f4c7b29f21fbb0ef6653e7d39f206470bd48f65c2775620953a2e5dd37a06756027c51c41cfb1f57b991e0106ff518ad0cb939ca2f4fd3fe36a477dea97a839ba6a5afade296c3e315822c83dceefde9fd34835d0387e0008a23cc5233c5d98c2816ac284d4c41988b598bd56b2babc5b68086175b2c70ed096891a7a1950304d684418b27eb46796d9f6b4566c069be2b2a68773f5f403bb44f7c70cccbff7f2379da46c93f482478e896fbd43fa05fb096ed79a6e24074f2e2ee81f59c668f8cbb3c33111365d770f06b48055ad789af571b1080041743437ab95e77a55f9a18fa58d72e18e62a1502d38d994ed68d7bd1e505fe8a7c6ccabb5cebbac780f7fe5b899cea0be8adfea88b57f96ed59588bcc716b31b7aa37127343e0497973161ed9990992fc008b5401665aba77e7e0650400357c71c7972037d7c75da64aac2211257cb5bd72de29048ae344bb59bb01a5c32136ccf1de8041455e6aa80f50616d230520830a5b2e865d3cbc242310eaca1350cda1c98e3d5afe8e6d30613a412c32f5bd1bbe3b6c6ec805464f5cc9d782f7b0b103820be5bdb7465375e1c7ce0da0f86cc859ddfce4e63116cf6822f0c77936df6a9f96312a5c962ca3b87452878f0e0f05b973ba95881e6f1dbb4998360df770b5884e2222e03f92085ff0608ce908d5bbf66596771d6919738c2d7ecc549aa56c7f3cc29ca43c1c150faffd960095926699c4e8ae824935b5ca9eacf340c645b2df9bb8200da3f9594e22be04af09cd37ee127d976fef57054b6408aaafdc9942536a88ed30889df277b9e172ef214d92fec729c0bbc5e6a1abb986b009d36bb472fd908f06aef288deb2a9f4b5e3b9071337dd24a6a7de74b9607f0aec2b208b85cf412764e23dfe6a0547357401a50e80923272d318783c994a19bc42235f30439de5746882a835bfe2475fa3650fb8659eada9d719345749e7b6492fcb0824911455a727575c694e5774e91da8ed484f0591e9f58f00d92b832d546a892d007a7f9ae207209c3ef6df4d8f85d6e37fb78bf6ac1f28597a715574c890c75bf23fd792286a3d601c21aa7b42177e8f237df4011c3d96d1451a1135faf02311e519bb5aa079a3022a6df4c55c524b45f0fa2eceb7b9f21668a43a48ad8ae533947670c32d6709629b0a888bf344d4775ce5f1314ffcb219076136176d75cc4f234a93197e9991779e84fb2bd6d867940ab521269070f5c8b4ba1f462b692593aa6b5bcc963a46dd2f9812a0abaaabcd2269043508919154f4b9be5738a95abacb1920e18b6280fcdc8375b61f29855f6202cb2c9bda96c477eccb4ce758e73e165ffe68ad7d6a527a477bc4accc57635a70d0ffa664da66f5364b72a23250092ced990d17ef32a3c0176e7a1b5c8112ee74e87d8b0e8ec883cf98850f3b45f15dd704c1960c7513fbc2e278aae2a80b69e36efaa6e85d9feacac7e05767ffb9895d7a4a613578f3855f68b8ee8dbcd6c7e39da01464ff0db79e78d4760943a602ceb66c9f0ef95c07941b7722fe0db4e6a6c4027d7a386886f9d050148661aa601b77391138896b3c6a6160ea74ccfc10b457172aa3632c9f1df55997e50bf48727974cc5545f1eb891c0de2c8b08d71ea698db71a0932d474075b7a80d6a587be0529f08bac6fd34f81407d55db6d1f0b15d4b5483bbeda6eca1bfeae73fa75f0e3ecd1d04788b7032b32b304df2dc3b79cdfd8b5ba76360cb052c849a318baed8e53fd989746a5a630a3f805b2d30b872df3b30e98cbf3c77ce2b6dc57feb82471b286c6c14aff183c92e221f22d5c91af4afb6a9d1928470748e1fc51cc0c1a4f4cf8e9f366cd6dd94092f690d5b5e9a5ea5d710bc2893a69f5d04d48b6905a41cffddc505cb1478719c92761a8bffcf5a945b6e435fd754835d01ff7e08453dfbcfbbd84519df378a9e5a9b2f0f58aa97881dffb0d4052142ddb0243ad83f9c5f0747d82b7d8c9ae185ec9263b94cd9941bb883e762def7ef631cb9f29d82419da21c1c28a8b2ef79c5b2a332fa6b24a35e45e59f19962f48c3d4ac9348bfa5599e60f1f7e804b612f52b5fc64d6f30119419164d48f69aa3e32237e98b948f200d6d35000e87afa62b2d8a3632505c0f66cc9b761fa9ca6f5bc973f2de0911a6dccc596f55bd4e8fd456e25ff3f6323c62b1175e80166b87c192a40ba9c42945fd0312fb2107c88f4bde3b26bf70185f003a239cf3a0afddacd98cee86dae79aece742b528cd87dd5d8d0afc0ca7101692a9d281dff1ace8bab0882b73d75352c0278a283b338b6484abb1afadca0de16939bd4a23fc287d7ab248d0e6f1ecbf78a15582d925b38f5bc0a50d9a4d7e8ff959a104680d5c718d2622ff7813534d892d01179397d27aec6f685e011b5a5552060e649eaec94872b79b2085fd97456238f19d3183f576b8309694a22f1fd5b027842d6413f019c9f2ee9652536473b52c04640c3a64d3815a3ef17601710f98e7c1b77e708e4c0864dd3b30acc3a5e81515ca31b5668b0c08e8bd74819c77dd306add2197736b4a87bad61a44743f9eee10a14576b5417cbf631916614d8f1f7dd58b00a9ed67a385709ebc32a5cec9b4d69639c6f7dd4952aae31fce7c8345ef95a11853ec29170cf8a50b19c8081573413a35f788c3dfa5a5f44a99042a77fed09049efcb947d5735c0cb25e67e80886d3a724891fcd4610655b3ff949448b92c6c51cbeb7071d24404d8e8206342e0f295a4ab9b305e21a82a70c61feab8fc73f64a3800d310844baeffcb046a756d7e19baf1d034baff532672e25b49f251beff496eadeb89a12921cd26d3aa4142beab37e13d781d1bd339a0caecc3cd773fe8381ff4c17bd39bb65cb948ade4634e070e7b3d38d5e46dee0849d6fa723dff9ed7a09463f8cf184ef048e3dd2d9d0c309b29b4ad992d916281d07539c7a669dbc5d86a08573cc0e768c4cc7dcc733d956a61a25dd90f9437f74837fc3dc881d24557cc1a7184e7c3c7ef2fcde03360f69d7f8f47d5b3427fe64192b18e79998b50a9a46f247f6b5e951be76241179ebb64332590230c6b4bda0a63b711b0176f9a208f599c9934a3ee65a08eb9c9aa89304b6c12d6cb70158e7a9489fdc1e2aba6e1d065222da6c1983c132b94d9bf3e53aa642af1979369382459c698807aec5a228c0f63f14599604341238cde372716e128d94e7e6a5ff1f291353d8438d447ba28a7157ec1880c324ffd78bafca701ab3e960e74244423f6110f80fe9decbef333c708cc802acab242cd0d981960281a5532a1a937c912126575baf9836e2526231f42be1a41d8fdba7bdd9e5ef7999718de49fd8569181808b42a3d6c6b8205eea8d45e5c876d2514a2c4f4aa5f92949567a3fc2f33156e322f113370a294dc1e38b3446a0b5381daef1b94d868d0205d568060b874b8d8632488d5d5d50f0183b8a6edcd4cc371a128148c1457fae308fb67d5ca6c8d3e03b23c9c27e019645e8449217955694be8f30333b9f4335a3bdc8487450b2639c63adbb0d721d0126b4e6d6a340e87a6769373e2599ea893d68f58458a74a5b51228cc65004bc49615e02386f502427bb690dd2030e3397f09db71f273c362a670e66852f9656336c9c84f11948306523519d683e470e3cdfcdbec7d71a30ccf553bf88f0aa22005b822422a0b8e5f82ce91a696b01319beba3518645acbc27d4f29e0dc3ad13655121491c58b1ff4062615fb71e0adb9b077c6edaee1c0eab055233429918aadb84c46127a91d55d41244ab59f38012ccb9b21b720ce05c9800e2fd1bf0d67af6af16dfa859024505ccf4e45202de987979b4e33a57c820090c77269470da17721ac9554cf753bc76064e131ff7f00beb54a1e944cdeedbbe244aa7b02c616fe27767b3cf9b941b5a9ad6698269f0210306e12df69928f53d9edfb47f6302da59d4462bc2b30ed337774afb1bcf822952095ae13ee1937d713a04cc2184872e7cbcf8092ba149e90b94dfb9bb41549879e5ba55056c98e7335ef4102aafe7c134c94b3d6a2b55e9be026ffe0510c822a2a0545ac4c1d2c19aebf5e6a58af53139b66709b01a544c3f294b3f32ae6ff4cb5592f7b05602e546cd358c95f48101476fab309b1db90605dfa151a918755df16fe1ef867d5240e3eb7430cc4b5ad7f8bfe3cb5d3810f1f031cfffb8a86bdbd7cbf3826a198bcfcf4c707187e5fc9305b5c2d3e767172feea6434e6df319dfbe1f590f820a6349d857257d20dd02859db29fbb629b49aaa3215992fcd9e5a443a5db9da5476f6b45ea0fc0ef5b97fd967c3050633187738f129655d4b8049b5627dfd9006dc03022e78a57c9778419d0a11e19d1c726cbd777dafd223161c64f248b554aebf95e1cc8642d1d02c42feac91e366c1c37120eaa39bb9692ca451af2d3f82d2ed83b00751e17af6568e56e81cd5aa20cd243ce5ac1cafe8a5bf8eb6d684163fadc941f2ad72bbefe40ef6234895b4249dc05307d421eb6fe665214fd0b56daedafd6a3b4a30f0193fd749b2c645470fca4b54e1d0c8dc4609577d21decabb84a4d66a841493dce327d35966488f6aff9f3d3eab169646d5cccb142d5fecd60f8967b43d9355b7df00735aea38ba7f56dc4fc3a8c9d1b1e708251f62fc86dac4f7fdaf0370673ea25332c15b46de72fb2d5e26fafe60d7c587ec3e8adf31152ca1c6a0468f2c7be430154c58d8dae06683b0c9d787e3d7a9d04d98d253116b4951e3a5bfe76b1025dfef5c396ba19d56a52575c1926ef26910870097cb464ac3da06196b2964ae519620e5fd7361904a2f10757ba1efe4c358035ebcaf73e7b62e29f81b9e840bc8ab9e71e1797dfe8919fda159a547ddc5d7dc98c9ef5a05559c1b92afeca6ad7354c89331b51c81d1e8d76fcd305e6c3dd7d60a8be99ee71d89d2084c01131b30a4e2c22184028e70ac74f262c5f84697831e9bb979f62b9f5ab58d6ec1d63e19008444a80d83d645a556f7dac03b145a41634177e388c2c019242a60c133e2eb08d4c3e3e1521ec7212b813ee37c60359407413ef3e5beeec22b9af315c12dd14cdb4c39a8531504246e95ba0e184c235bb8bd37b7cc8ae0dcb63cb1befa0ff10dd21a69302bf3dc44f24e4cde95867b5f746de4b6a6460dcb6c5c75294e21eb94ccfd61b7340342ac9f07c83a1a6c3e55aa866c465592ef174d72543dec3a512e2bcae7ed5e5659a3518c49bb583bdb983cfd202e54b74ff5538867c87b97ec2c4ef935fbb3198aaab55b1c3cff0a479dc7a3e373ca46dda796a1cd929b6fc4d283c0db82c604502d7f10cf60893213e87893308335b39971bc3129fab34081df3575ce72c0b8cd718f95464aadbe28dfb2c6320f8d81a28ee4f5ab19e1397e0d1da0320cd78caf08111a7dd97ead3ed61a44e895f3c84abfdee8cf3cdfcaf4adf8885715befbe705def01b2404439e0cce208b2b469d0cf66d3584883797e6a32e4c212395c8b6270a7bc63024e461bfb8a94f8c6625bd1316e97784c1623b04ad25f1f3d978905c4d1147cb33005471c87bef2c945e5a62e0a50297564f7c73199447d9352a2c45ce7a1606d8e0e6b409b56ace5928a84c83835856d887ecd921145240262ab61486467c143497a527a1a18db155bc1eb7a14b59a33f6c9ccd34ee4a01bd9b49ffd24076e5a745fa6e538e04ad2a6854477f30b6619d54a4d4099d0f3e3cfaaef96ac24547017d25d54f2c7f133f6ebd7a98893955b9f8eb0fa0b2ab3cf784cdb3242df566c839342ae19855dfdb0029a4aaf248b0896a1b9b94c54201c119aa1dbd46cbcf6d0517a36de8565c6913d5b425595d93a6c10d8d36719a5dcb72e0d20127347b35a64f230ea5f252d2362a69bc626b1559f72f463e0b8caec2bc81920029df4a93e978c67bacf1ba7388e702ba7474328d4da0c04b232e26b9ae58847e49c627fbe985d14989bcdee91e595c43f366f0dfc942959e75e3bed98fff765d7f82f2b1e34435b106c82767b70f8303f58981c7d489ea91837582fa26015564d704eea711498c86987f07940eeef9a6528a7251250a013033547f592d5a828e763cb9f1a645a77948ecc44366aec787a0c394b0be719e7a7526ff1899c3d9679072c844ad71d42e9d28a68316812b0b0d19f62152dd39b4519e32aff46700d3fb62809b4d917f080f5ff6d6de155b3d9318531c43f18d74fcc50e1a1d55dc4c3118e4d56682ea88494adcde1b7a11c21827fbf1ed5de1c3808979801d1204c4a8d61b9ee5c81a6b4ca37f9805c49d88af784707b2638d4bf8641554bcad0f895ecd655fd2de72eb23b95f3e4c7e746d1945715c499ab1dd02fe31cbc2238e75368e02505c372ab86e33b161495a18f25a821357d25b7a42608d7bf625c5e7697847e72c54fa0496c6b43f179c6532d62b253b1f2f6431f9074b1187ff5ccc3c7fd58e7736278e1d753273710171bc4630abe6597eb3101869f6490f28475c2cbcb1a8ecae895d082557fe93eedebc8ef8ce59b86f45da2bdbb56e8f16314f34ac9ad5e1ae2db84d7b7b32804c756dd0b8eb2ac056ac6092791e73a0724faf764c3fb6b5f1376d6aaffe1577493dcb5319410de331fd6a03e1e4caa7e1bbac06999a7d5c92f35660e43f36dc292b4c7202b6e163009fdef31ad110ab242242d4f61e0fa9cb026d11af570bd6ee6b265eb5c8beefd55031a317cf7ac1f7ba2a6561625024322b04f04e4c65d85a7ef345f8447d790a70a566e0a96b0f0d3b84ad9dace3bae4724314fb7a3d6f3ff2ccd3d7ca68d2d0b13b4bb2750403215fd44ddb1ca042c376b3eda530635256ff6cc7a163c0f0233d0933508489ac2f55418be907ed28e95d7f7b8c34dd2b827fb844f779424e5fa9dc3d69e2a064f73b595a7d254bb06096893100d1dcaa20ad4df6800c3f381cfdb29d79fa3f3ff9700946f4cd3dbfef6e5bb5f5cc02bd3921cf7e5a317b4ac5737908213cc15dcf7642f56c7331c81628ad8cf12e86e623404d62f033ac2c7fb6f79ecbca8ec3432484c57fb095e655d1225c6796626e69cb2ac812fc85a9ffde2ad5ab9909134b817dfdb9994b569495bf142eea6bfaf8dca795eb28f9064303b831b02c6371948d3aeb5516afd89595adb5edd2beb4bd040ad65f27ed3a5e68c079f543b9a3f3db097ecab6a52f9a9ae56bc47e39939ef60d914e68b2e837088dfaf190c8bd5250807caa55be3bca7a1db4cc15bb6105d5f65aef8eeec87a11ea807d6cfae6cae81304127837e789a040e33ae09ca2df5effab7aced605d2e6d38837bdbd108e06b2a3d83f652fb8d9649ad35f4655a72f711251f9de62ffb64171ca0186bc585fe9338f62a5843754ebf7707a7c339a2727e7570e0b0b7f86e895b4b705fdca34b6ae2e67e5620ac166d1d475f709f509bf6487d2af84df7349e076368bde113e0f2bd5ac66a033df72da41715fdaeb060c007e1252509948bf0cebe7faba85efe139c5b9859f2772b08950bd05a69807c7d64a56b2ee562bac54a7081394facc7a657ae61fc6539c2a7cc83fd370f821bce8697db2da9aaf0f1b1704b1a1128bfe1c529cc734ce6686d07730e3443468be3efb1fc1baf1a2ae2328b2cab6a63c364d99a0aa1bdb17d046663ecb69c918a8100af17a248bcee89585410c9cadbb3b3f729ba54e4e388ad54a509bf95dd8eaafec9864ca5ebbaa29eed1d2715b1549ef55ef27e6ba5dbbc4e75ad07d2eacb77053efb617069cfe863756f56771c26a9eb0e099f025e9459267793b8b8942bfb5a24ceb6f8a6670b0c375fced5a9d6796f0412f07dc95723b38aed9efafc8856025c346ff5f62aa37cafd6a8c15f41180b6bdb8cbc3a1a63da5f83b079330f2f63248b6f7dfda844edb6e84c55bfffd4b136b6c5dd8ac7fb43b3ac24dfab4bb5b44d63e933103da670025e6c4a1e188080b69a3be36e3f85c3025aee28c98e44fe024d8b5c9fa2113c49c85d729b5d90866303d6ccdd000fdb1f838cff582b85c37414e44037733b4df9895fd0ba36adced5fbe081b2e4402303897d20a2f43f73ca2ef83e27f89362b6b889fa03c97b4e5bdc820bd626c5108e3efa31e4f89e84255b90903918df61100d6cf0e1f5676209e48067adc5e4d1ce1f67db309f52f352b5da0bac540318bab3238e7bf510dfbfd2ea581efd95f9f5dcdb3be5fa443b9127aa951320b4353a6b4d72505664aca586d9f038368a11012558791d3d64c113bf2c425b00ed12f8ab8e40d9093c65673a909b00b87e71366761e96a1fe7d4ec8f30bf9ab26299f64e2e1246d05949f27b29d270806caeb88b520c28b3f8ed4281fcc648937abca0558fa6ac8cbc4b757324ba6970234f7333689ff021543080392b9805ddfbc07cf5ef181ea109a27ed583d9d6e6eb6f94579c5085e4c8fa77e35fb76a2237f7f271629b19e6a482384097bbc002e43eed669993183b7b90cf3c753d304a3e79ac755154bd91dec3dca78aeeb1b93f4a8daac903057996e6ff90d1adcf0f34ba1a7a0c9eaaca15d7240aff7114e55f3247352261b5dd4e3fabec3edc4fdfaebf67f1e6e210045547261cf3b6ca249063bb83b7a6076f4c3e0f8be9b367d52408979e27c06c652b8a973f7d03ea0334b28336d8f76537e8b2a2bf2afc8a13fdd60e983c53ea155403795f66aa31d9fe7cfce14d4d7b5ee7f29d6c20726b2d74b41dc2c47b30db9b58de6ea5669ba533ad11627f760b2ff4c95f4576cd282870d1f45faa004e0b82c7fab95c1a1a9356e5593a5b3a5997d0072103af732d0af31582fdc6c60fe4ad1858209fc0cc6db67892b23cba0834ffa9a303c42d58dc4f4d4061de5081a8dac6ab85f02d1e74ef4e556eaf4d1b92a61ca9381bc90dff4131461246abc49c269d8da4cac2248a576734784ffcc05bcd73cf5bf16735b5cba8f75606cae488246e78fd8ee2e8a0089f5ef44d21e72dbf7d174dbe4509d6d492cc5953d9ed118a8c1f860bbdc200a596f0041b6cc3633668285f1a4bb0c7b32a9ab2eb10b96719100093a29257e295bb615a49a9942e0bba6b93239052368df419006fcaec94537e8c0741e6710bf877d88b0d181333da590c11c8957ffb94a8f4979a68228a3b9f045f23b6e39d55d678b18231e2c245052cb150e1ce413497dfe44c8aa90b46c9cd60c1ca92157dec7abfc998891bafe2f9c94d167f970991b2471c1b897c03834e4d9a9887c3a9715effb6c95898e45ef6cf539fc89d845f54738e5a02ffcd1fbce3f6badd8073a84fbfcdd9e8f9fd3fc12a1f4ecae05fce5cffc4f7c4dbca46ec0a8f15179901c04f75aa98321f765505abeaeb085cce98f8464e585298e3acf0afdb2406f4b46e12069bc95e02d6187fd1fec35195c016a77c5ee95fbb8fb091ea8e54f08be74d0dc031521efd1efc0bd3c15ee5ff99ff3ea9325f9c0fead24284808c9440c2063e4d0083685d76c81068c63efcdc56969d2d37195e4b9b03fe438f83890a848ecace18da471257bc11f464849f7268a7c2b60864ae5f19dde369fade9258dab06cb0e6d6127a1018945ed69425fb894939b36753250e594850d2c8f34d6966493c4c7c95a7f8cc3e4b3b5b4dc5c033bcb5b063c14eab0af0de1a478959f6e605c91a5635c07d9e998becb35b645814ce1d9f2cf651a6176563a88960ed40ec1d2d578a91f6aeb7491619c6afbe2b9d29a21035896e72888a26b4b8e711aad3a22fe41b2d34003460ed4a95995a8b89261b08a0245ce7c4129fcc9ea6d83b2d44be4663d1ae473b3353e32362a867ee3b518c5a0eccca4b3a4df77df23542587d0d37368fd8d3e7325d780a6784e19c59a78bdfdf846d27edd54c0e14098378666faa3dda2a26cfffecfe547e82ff6a1596fdc09e94b5c443bfa205496af37e52e391907207a511081c11ed936a1ad6eebcb8315ed975ddea8f6b5603731d3b8405bd2faed72989350ff2a2212a6d4ac7a9cc8c4bddc8ed3db4efa04c91a365140f25197cb3a93500f8552aa7555ec807087726d3dc8609eb937d8cfa979ca7c3dfa86876a00840554ec157b1529671795a42eb4340049613d8263253c814e93e0e5b3c3bd07ebdd38968b773bc2dd3cabe4652f8f652e1bcad0f8c0d0d2ba92803140218d23b26f40ea2d95e496e1fe6de24d81c41d1094962baa208a29e5a0e8b5cb591825f12f4c6512aefde75efd25f725961a4f86d1f4781f48536d096065182b805e7325ef29062facb77b3f8c2acc07f3f6f5ac80f701d713d95c13f7f656e69df3dc1967f20a2917d2f5b6752c854231a3ad69c68c8918c14f152d881b120c24bd8c93c66882053ae805eb486ebb323c586ea44ac0c102b71952373170334bd2632d36f452521b593c2d2227c4f3ceea8654a0d71dc732459b8fd86d65f4c602b34e97ead95028b31cbadcbd57d12bcb5f651d95d4996e46cdeb643bcb8c2856d696623e9be5f01c8fcce05010196cee77fea3077202dc5b1511d3b342dcdee57fcc09c73a3e7106cd5534cf87fe83b059add05ad0b6d46fa1305818e9eca4ad563c6e87d942b16d8bed1e965c92a34e79688bc8f7b504c8fd440c9042586cea951ced4e90a624820c475ecfd405b516e0f82ec0f54b8dd29d9e5c6b85a6117484481850c2bca5e059d640800c2e2ccaa85fbd41667a87bec9985374d413fe6e094bc225b0c1a5d35ce45ab894792d56765ffccd646f1b58aab133ceb6ab044cca0494749cb417d1bfbd49d953a20b8ac4ccb90281141ca933def920f1310035e22efc01b78ba1afcc030f56a4ff3f9980a5daa9c4e0f9b3d6de3ecfa1c116900fa8b5e6792b89963ad53f9b882abbefc2e1cffbc3a184a69682e1e25df96ee180b31cdc89f12d972f222b17c378ef50640e87cfde98c4f16226b62cd4d8fde72b1e0718c10d1cb74c1e17c260e485f546c0633f904d0e614fe90418859baaffab181313d255fe0feece64eaf4645fbe4a9020bbf22b049963627d3a2b424dde7dbad7383c4182342914ba15fad6c42532f7ca5f49373bd1021bef7ff95adee8e9f50f1427a27ba81609cd11921875a267e414481f7aba29f2153c73baa675e28613f167aa574a010dedeb43cf292d1c77295de460a1b9860fb92752788075f4ae097384ce6d0419fbe0e9c1bfcb8c0a211f60da7a0d75be04a3456f4bca480aef422269e599d3740659f1adda7f2b8969dd6d5c1e995723bc994aa94869ae252d41eb2938074abf329f84b56f2cf464d72f0020f09fb5b897906ba926628d752162c64e1e36bbb2c05bb2459d3876fd40d88da98896e4d670792b861e7948224c8cd14533d66d4bd5c9e77f0d37e1a2919cf16a8cf50fae649c2089146256820c39b3b4dffebf75c4ec25548334a229f4dac3e756d57e052b7e1db9225987c1e8f62b02952bc30030698ec183b7f45124c115271af779ac049b4d0e9769fdc1361bf6bc3917db2d270c787bd4d2daf6fe3e6678e1ff2e01b5e29b4260445782f9a751ce7d3661cc42864f5edfc9d31d85dd6d5b27ce443c5e8b961c07ae4d7ee26e77e30ca03b0195405d152ecc94553c61cf7aa58df67ffafd01c34e9e30d126bc6a73e2db79c51c751e87b94b754590533c1d4dcfc480ef946f87a6489ef9d75210deca9d95775f489fed113fa96848ec25e5876912102940e4c0506c6695eec7ed9379059a1f0ee06c52ff90504a42a18e0b42abda7b1254a26b258b763507df54fc9a65f84d9304f6e1ec6dad1fc890d941fc426015e8846f6ed57ee804fdc2d7a81210f6203ef57e230071d73c0e0c8149c9aef7838f5c6ad882724d274d14807f0d0d5d500f5576943a60a5c493534d1c8daee778e56bf68b6af236e047e9272c781fd0f99d8fd5be5d0421adfefef84f170f32e4c59810b3b64e7da098553dfe1ae7bf6324153c268c3d0c4c3f74646563fece2ab210deab6f4fd8d950394d6d8f525cb3000f8fcd5ca34af07c67627c1214054e948684130efeae1d89da0f8f196bcec499aeffd024427d19a51c1aab2fbff4e67be500c5c051c15a9930cec45f9443936eca7794c31e44560e73d4bc58868738ac3377247c540217e363a55db860668f3830cd5a6ad18dc7c1ddb6eaf131a972cc50d04ee532f7e38b3387ffa2b71dceb31f43eb1a11c12e2ce673148a741d5aa77c0555e088453a7c51e5b769846b9a24b90dcfe4dde45996b07c712ed2c60ddebcf8af354481fe6188f80539449cc60bd88be4e453da9e643c46fceeceab91c3dc5a68ad1225ca86cf060e477ef2a38ed8ab157bb60877d4e41116c51b8e038126ac846497bad9a090f1ebf5836e925c4c3266e641d2564a95943396ae06e2472f34a662aad8bf742aac3805694c499773ee1ae8b2c2f9002077217eb32800adbc9893dff7c117868f22defec9fe9551efb9d05dbb9617a1bb520ef93f3893e2e3eb6cfe00ef699f8c5d4464b309104611ad11a13e04898510ca2fd2c417f16fdcc9b9f0ed620198bc8277b263e53429de3440c60e5f27928c2ddc78436ded94d66be7e30556fe2048c667287108a6dcc837b89d48a98c46b60f779095081e8f96fac1385cdcd51930f1cd2fd58781ceb5d0ab9f2652437b39f654e5c43711f5a1a6a838876606fc43b90342658c35084a9712a7269606dd359a61a7304a10e9156aabdce94497900111d750bfe2e7998449347fd9215b216c1acedc52d3993cb757b667807454acf8f153134d1a274ffeb980340dcec9061862d4e5a09101e9b4a13a40ff1ab672297769029b5951a2eab99be8da777d414399df3f452bab0a83bb8723f234a788a14d9b13c6a2ca7d8be577809be45ea7db7816e972a18033531039367e8a5ba4cc8994fcd938add9389c1a693f2fa914d36fb1c905e443c8c39e0da250f0842cf73b448b38695376178edaf3eaaa37ba5404a7a19d1013fb394e1131cf3641fb24b43f3f681e6b4c6fea14879331c78aecf295753ee372d76cf1a2321277d96d69e46192aa7d2edce649233241bd014c627e85c9107c003aeded5e8f5f1c0f6ef72aebf46132c3ebee1b404e50cb547780daaf6820853645a04175bae93a1b235b76a16fab3e29a0ba42cee10b2ae5ac87a84904bb98faaa341c9e15469b9080fd410db5184e086d53955dd57f29b39b10a090b00f667ad26436ea339cfd7d24bc01f4f44e6a0f855a1b16093845b774539bea3b4f49252ce0277e1d3de70efe07c844ff0a0742fc238d5ee1bc2c6758420de63190422815cd9c13abc55977211fdecef76067d5e1d305b66a3554958dda7ef42dae3b696e9bb3617d3b37b550ad41b7ed425d3cd7e0bf7c17b60b48ed8aacb118a6d0f9874694c0264c7b3931a50af646f667bdc976e88c034b943186660d71c2a1983d686e978196b094762bfb48aac83f9b68de63b067b84a75b1c03ceba8c1e94e6d43a220457ba0164eb7472eeeb398b31295936a3c508bfef2df00dc9aac47f40e5747123e9b8c1c37c5f5588357d3bf2801432eb87a15b3130efd26b2b5d10f0f1c24f11890aeec1ce20c0ab1689728e4db66f2a0b899e70754e7807f5bb8e58d6a4cc80c0575497b73a77255b9ea277d3ab8373d0133ff7e0adbb1436edf875a28d9eaa8803614a3bdfd60f714d94b99d31ca3539e7790012c4f4eda54a792dbc421bd632fc4c1de03e832751284f70a78ad461fbbef6ae74c755dc2c46be1e420ed40c25d916fb850926db7cac23f645afaa7347155d64e720ed7bbf119c7a0a01051a116b8ab4adeacf48d3f928b031abdab12ff3e333dfae1de4ded67245d9442a2b0cd63598691b1bb2926f7e338d38b35e3f7edf90d3836e194fa1e01b8a524a40603d780220ad3de9194f5326d40e5be84a8af4c18d1b1e48efb48237fd22e60fa02b36c95dcb5fa9cd8fac487f21a6d5e88769d86bb8d9e95744d5829c88450e1b26968200676a7968b5b66f5fd2a6de25769afad00a78ecf2a20e8a864f8298ce3950fde0f17d9d2a22f6b6ff94ed3302a184bbf58bc97cf67ac8654af10e0cf8778eef1157081eb752355cc67b54a9606ce0e7ee65f024832c4f3f10a2aa3a369a75e026f8de64e08bb11ec1cee370dc2d3c15702bd24e6d068b317038495ab50c5b1b5beb74d8a22a4e4466ae0bf5e9e503f59064c1f3061658db7eb56ad5e7a0bb0beb6b54c994b80be1045934869f3e877596f00dc4f6d985ae60f452df77f622eaba13bc439820f976c06e8c8fce6baa0cf84845578a75bba469aab504f64a4f126e6d606ab3463e7ca53204cbb866767b6a4a257bf3ebcf8145f7513f00874676327dc7249afeffeb3e8b6124a64a2b954cbd7e4c7d2b31c5a0ca7baa4c1771757e73e8fec566dd07b9ed540c54276e5944f8e6c4c8269a154bc66f7f37b5108a525a27c1c50502c671e4220b9cd2ec45dfe5aeb391193bc769d02b8515ce803be333776b4e9ba8696a56cc04f647f2e412228b1973eee56128b7c67662f3a5b57e4c3ce86e53eb78bb54e64c819e39127fed7401f4f6a69ad63e44bf96db0f1ed91dfe6e55bca688d194774dc310b080d79292a835a9b52ca2a035b8faf812e155d32aab9066f874e9379cd27a985029af12b34c559dd39fa8f1888497d531bd067db1082589f211bff06bb3bd2783849761b8389e0f8bf3123d6368dcd9d55025b185d981d9e3ed770ffeca6cf5dcb7e78d638c6e14b75786085d05638bacce7dbaab0a3ed10c9db92a50b6d42393f49e3588f36ae1fc3e2d26d52c25a892ff39a5df799b499f01bd7da4614b90da86bf315f8d41c01d950394c7049a6ad9cba587e33d947698128a0c8f17899b3228cbc7b31d08e594af22209b212f5b41e957efc55dafeb6a3dd4e3b01d22ef32037f53f375e182b713e7d84d1d78ed259150f4b22a929f2687bc4f84f5c9ec33816a28a17e95b6707d4e32c7332f54a62f0aee8902f5400570e97ed94ec813e4e0c1a92e3a80849d3bc5dbc7143e2b6f88c04d1efcdcaec84ed05284c4afb87efeedcba0496d6f7d89e7cc92374a1e42ab71619dc81bae2e08c1945dac5de23882ac48c5aa842822353e5b2d9b2f3a210c9797751b0eb032d2494335c6e7b074aef33e031454de9962b15045231ddb51b75c8b4e147f48419540ad7ef51b16695b71c50d527352608b4e840866a1feff886506134d8895961ba84420807845eb5d09c3346b44869247124f327e42be0d657bf344763d572040e373bfce12d9c5e4a18f09db055afa02adf9367035051b3a38fc039ca42b81771f9bd4929750f0578929f2ca6025e8bc930e0b7bbd68c4f608d36c722113ff5aebf976d21af756f68877b74410e74e441a7f9c47e2725e420c865f13829183728dcf48e48b7699857762e6a913ef2a82e36570a22f908fa30fab7c201eae683335182857c594445ee51fee80f561a2ca4dca28f86f65012ec52bf92146fc15c97c921c18d6aac580fb93d670a4a3455fe421759e3c6a9218af3678916f780f8b77585e6a720ad82f23f27a08f6d9c6c1df8b48bd853cee0990b427dae57597fd5efac4573e6ea1e30c60494ced16b8d43e172c00ecd5d22919353375291abf644296587724aa3a5a4d4f33ed4b239f0c2c035d2aeac105714b20c2df97fd582b7eac6df9e7ab667e5ead29ad37967ca6f45d81a559b9d5be55211cd500268c8725ad98ef412a968910f61adf03b5251c62c26586b613ddeb34c0f33a4b3d7a9d09cd3754482bea29ab79e2e0a1ad675b861dcfe3e529219532a18a2bc192ef95da6974de62be91344fb648b813c0e983ad8f1d8bf06363f9e12b74f3f2349fcc70c238a2a4b79db234980c8678d3ee31c32fa2906a4eead759831ef17542e83cecbdb07e15085e3b15fe08eb5e7dd277601f9b81b583ec99d2b99e8f4034159bdde1158566cabc0ba336d29e6385aab5379f2777a3d62a8a953b956d44a0cb30e0a32f5fe866b72dbf1effd25ff4a918663e93bcd10ddef44d03f9c698d4004416d990e0b4e32cd7fdd2eb247fd4b8226afc4e3a515c093ba285ba8883372c7c99f7c646249da0c36ed14136a21b269bae0f728b59192a37b0acbed10f9fffb67000e5b312b254bf64d4345c4709b30225f6235f1156cdd7142e634ef628a5b8028e61ed330415a5305a29b097bcd01829c00f023a7c2ea06b729287d052480f2b096db7a5ccf7641b56e83d24043a0171d825ecc6f2a902059b47836b73bf2b392ddda2c31733f71254f5fd507dca1dcbf635baf4af3d1ab737a57595424fae4da851b982015d42f3de721f75d0695ee42cd8e0c572124c5b0e75bd58d7465da03b6e9b2a1da6b83d53c115e94972e01e44f500161dd8cc616eaf4138196faeaa3e3bc9342c4a4481968b51041d214149e2aedddff9926076274545e25b4a4faccf54b528d1f85aa9da0f216921f295845fffaedc8575dcc001124cdcd5a101c854090206eab1f0a8d3e4a46d80e25a1b627a7b89b2977e32a6c11a0c196bd05e5d93009f67aaa392a8b37cbf36ccb2be242d753202217d0d38e75c24187540ff0225262e0dcc749953f21a3d20a3c996cd39e483a9fca773ce73af42fbea04a0afa420ccbe517c30db93458681ad3affbcf125935e46945672ad62e37ed6a9c77e460232c1e626d7b203f3cb2be7a1de07844714803f84061e7e36b184da49fbd86aa9ae41aabe33ecd0a98b8bddadbc55725af7a8bc04de6a78881bea5845e91d2ebbb4a63dfca1453c1957da6518a8b3186aac654900cb7ec5aabd6c022bc41208aa59c4841ed263706e1d3c583a8da24e1a50a851c472e856d68cbde5cd92cf1528ff28aa3d2687e626d85b5390957dc58eba750adafcacfe090f095446267c4c1ee041a7e5ae288e97004f4956b1eb86d9256f9a1df93f4c8b85cc5e90acc32972ae55c98f2d493994dd4897a0dd2f43e49263a72fcfa622bb73fc208a69366d06278926650dafbafce53782263f247e5b2d65dcd5978bedeea197cd1b02b9115867ce8013c75f561d6f321797d8c8d3831f43085b45df0abaf8fbcc47d921d3a6703c54175e7b6bbfa38d86ddfabe41fe6230a5d457f41fd5758c9cdfff5ca17ca1f05ce4c5c7b6c1c869425befe414625831ebbc57f383334f3ff5154c772f548cbc81dd5e8013d1e1272934e3ecb3ed845ad141cfd95bc0fcfc5f0959cda1defdf4efb624454fbc6e8811fe42a4b98b4ef4bde1af9ee2fb0cdb448b69f9796d54dfbceafed4db0b97df16a6f3e9097ba0720b14d3b394c0d3a9a59aae6a11513ee16bcadd44497f18d883f437e68ad592e0f402e598735e3c469e2e97b8f4625c67b188b367702c15b99b4cb76fd0e878acf07cff1ca4f35573e0379e5127fc27de63dce278de8563d05d0e0825ff189589ecd9a0e1ca6aab9e8848335b47eec3efd34e081c035b0012af5f853837f1d2862bad1a9c313264283cfdd1c831ad7e0218dffb0dd97c5402faddd9ecf1c60ab2f4ca1255c0d52ba72345655c3be58894b695137379e525f5db4c08a8c3a15dc62f44418aa420a2b1d462789ae43b6dcddde7ba3f6f9171b5c2a824411b2cf3fd9881363a25dde1cc536b0f4ff1b75b1b4bf0aad21a13cb66306c6fc7ee69c3f6a8da965e6b4058db2e1ee1892940c17082c936fa3d39a6d5d6c43b9ce962fdd45bd35f5fbb9276ef26c604fb485c3d1da1d1b73ef1a08ee4e3ae9e7606373892666712302088bdf1f17dcb6d78b1f59b7af4559f6e8d01cf01324f81840b2921f1d2c5b1bf997289a4ef9b37e229b6e59ea80d9998f91bd6b3c26afdc06165ac69b600db73dc1f3fe2d5e78d3419dd9190538b76bbb50c4e827faea0abdebecec13e568130b26726543ab8571c812ff5eee4dc667a1fcea936517b2b0bb27bc95bcf2a663cd40d5cc3261a131794cac5c2871aefcd44ef449703368d95bef3d2893c5cacbef618e9a7b23631564ea5e16e4d8143b6b330f0b790717b1034b6fe3f96caf730285f9d46bf814e324f515011c9266cfe5c80f58ab41d6a709084cbd3f56adde14eaa0c875b52822b60af9e2dccb571a1eb625aaed917889eccb127e1039664724bd4a457e75b4bb35936a980191c52e885e61bc5114679835c683b047030344aef49c00e966fca0ddf7798ffaf2f81ee5eae68662993f709f32ecb50ba44c522fbe9648af7ee71a815afcb07064200de05eea721b144e82bb8461c3b8ac7917ac620032e3634fa64b3cd52016ae3e6ebdd3ebfd3d588ec0c7ba130d3b22fcfae16d97c1cfa146b130b28f4253087bb3a032d29685e472e99f6919247e89c04dc6ea08a9cf1a50f99c44b069d415e25583bdbdba5e50c7aa6cffb7332fbf2a8374da2a9f0f5b465f3a5e4e2300eb39987582488a90fccb7cc69e536903bf95ee73a628e9a69c6b028d05cbbfcc892b6ab6e47990bca71e3ecbe37be4717ea4ba6e148639b24acc5d39b650ca3e451614da376ef4e0df057c8a52a771327a055e6e456b89f66b0a267a529563745d4ed227780837e29ec4fa083af66a5ed8a4048c1986534e7e6bc8ccb7350f9ae2bda3e8c1f0c85f9e2c946ecc8194491e7a9ab76c3d1756e7a3f7226b11eff7a9461bdb7f060d73e3bb6a703cb50f48432136846c75271f4763b2662dbabb01561a35788a7a03607f6ab6371e026296f09998656908290403ca2670888dfcd4d91dade2c466c2d041bf496e587208aa84063a41663fd5261f5267c6dd2135358cacc8ec88ff2bce9742cb33015c1a7f9614391251470ef289393b9a655fa51b61d123a5a05fefe6c65daebe8ccdc77e179a2305707a650563670d81c21c0fd22229b605042caafc3d0f5788ca6bbb9574377753b0f615daab245dc3698e93109f39c948524375e227bffac8a00774671a8e5238c9da673f4793959699c9862a475c73fa614bb6444fbaf512a51a5295dcd3993ee6a2a3db84f9136ddd6b2db14e7556ac100e9cd189013112aa425daa05be9275c7b43e35edae20b1a049882651f94cda5c23e36a4724673b673bd9cc1f1d5165537b84e70e62e2c0b339a77fda5aeb0afcbd21c43e2154ab31134c5638298e1b6b76d5f936f06ae4ef96f41ba340c672847342ca2a1ec1de697bf2dc7de7a4eab22ccc98118e2d2a83c457d43aed730fe865ffc2b1fd42ca4266751dcb3263e268709a2b6c3946c2c1e2cd3ec217719f7161497812b23cab90bf8f2144eec347c37840774d88ee4ffa1560be1006c19e5472139d706f57e635573c2f2e90f4c2ca7e058c750551da949f285e9f20bdab47ee74be5311b5998e8dd39f6a65f137ccd7ef0040aae3a57fbb972ed7de977b23de446ee9abf977b8244e72d37e171ad5b522dd82055c86097af0050745cd2dbe8b9905bccd6c0b220b10de1d0d282a77b6ecbff679a532d76d7074a74402f4f6d8e10319d580054467487bc3c2f71bdc9d2721eb9570d6a787165c9c26ad25c6d3328198f09b2214b9dc6c264f32cd0342dfc884971b5d0901bf058b1da3c7f9c1f77bf139181a024c80e12ce9e1b246b4bddddbc2c3ebff6597e9b59338a34856e90ddfa32d989e0ebc4aa5c3efa19311cab564b43e274c28bf988288aeb1da57007550f33075b1ed02efe2dd3a8bbda0faec1f3c2a4d3007dff53ca60414ecaa76dfc60e65551583c9bc78e47047b0b32f0c929fb0cf32e5116fda9372edb3cb0138251a7083b39ec109ec598b811a36991e7df5f886e4e1c9abefaaa2adec2dd4a8f37014fc57f6a677532a836b1d91bad274ac67eae6a748becd0ca79d086f5368b3052b9fc6f4c781cd8b23e63d7dbd3c36c03155f8d946f17e36c695fa606ebb903bdc9e3d386204b7799eaac0349842009a979feb231783efe428438ba590af3e495b1821eaa696692c8883bb64ab3c72905028f25fc48f64a56765a7c977157eec481d10f7029c8795e19eed1a372400dfe36f295521684b704bc02be7915081760e41ea49223a06d22198a69939379d1059ca65d5d44d5afafdca2cc637a077f4eb37f4a166e7d0b9bbc42042308ff27ede68e0b447e738f7aa3accd5df20a1d0a0b89cc7e34dcb4a18510c69d29850211b73bfdc24ed6bdbcd91ad6c44fd89b34d452047954e2fec94993d0d6333c874d984a55ff3be0b04d84c253649c314eb814c228cbe8a34758a9821a2cb2d62bc24f47c31f1455006b5b9e49e1e746c48f3e6e36622c535b36b218bfff2c0bb5a9c4d2e8116a5b059a64bcf48a8e0479df2f377a92cba97398df8042e84f4a4a5d6890ece53f8305e1073ca443e9b0a2fb00bd362070d410afa14788aa70a9744a16aeaaf6988022c1b877521b2598516d25c3848fcb6b457633f685db19321ab2305bb8b022025272065c2791c1eb75ec982232b246a09c3a893991003b35b96b63e5319654e0b668084f4106878e6af9bab709331f612fa068017483edac45a19389c4a09ed1ab5ac24be494c12a19b3fef554a98ae7c6a05c63ac8741fea9d006ed4680c929054d4c6e43d18dc53eb2fbf4c8b4444fbfd4563251abbd511495cc34f716d1188acfdf56f2d9e279356320b293bfeb4b23b41d55830514585f9fc01f8540e914c1ca142b6141e7703cae84aa91877a08cc435b9ff7a0e9dbd8be2a3fa69baf9a974b5c40c73e497588a9953c832507dc6d258df85e4cf6366bca5f678c593bdda4c9a5faa738c1741f39413fe27fde70fc5e3ac3a97dd1072d16509cd42ff5a02292483a34744d39fa8416505bf0176e210085e094fdc1c9afcd3e51fec181869c4c6586d51a4d833c536fd3dfefbbfaaf97cb3211fc597c56015181f28e7378b0eb59014f106147dee52c509e9be63d25dd92395d88b3767fb946f94aa5502751e8be32e4fd53c444c231c2979c538ffe3e9fe8c45ef6988fc0da2ccfdf9634d7b805d2836056018e9e6ed0343f12e9a66e90efbabc4d0043cd5f97b719cb68100c1e5f1cf78f81b0b7874033e48fb2ccc2aa1cd9aa4b76bbb593543d703a018d5236597f388cccb6cbc2d2a9f589d4e8e14a0ed796a2c7f3520a82f197d2e3b577af1aa75d29bbbf21a3df131141fdb8b1c7fed9c9246755565e82b831d78ceb982407a7656e2e3ef1188becddd993303bdad4445cfb960502a60052e8daf097ff5c9309dd7c41ad14b4d4ac1f51f23c73c3e010972daafd5ae6073c503b620725adb06bacaa2ca3f2d4246b6be07e8b0be9300bdae17eda8ca87e3130af546ac0504745a5666530f6db434e17dbf119fa1716c1bcc4f21e68fe4cc3c81b771f9ffebe2193451a06cbd4d975fac399022fbabe0ddf58c470d0b94bf28d3229730d4fe9109b16f08d2db972aba4060219d947466d657303e63f1c9f784f0edb6028f4a7053a7d4017237e1719e494e09a47cf10e9fced36c7070eec1771ef8b5bc9d491072e18a2c15877111b87d3b1463cbd0542ed26dba11eb3842a587e17d62189580198d1871af82bcb0a30e33f56c29861f23eb308a29d9486e310cbd2c9ec39dc9ff585ecec3b5e9f4722d852793f3dbec2d53afb9b3d7a2575cbae7571506db2ed5be4bde7b08a59c9017e7a8698ea38db5274a144ccecbb17d073939e8c837679ee86537442c91ce6f2fac251f8187ecb28b5acf7ce23de74a0003df0e70c4be7dc57b2a9ecbc6f4b86f7438a2c2313c66f472943497fd111a5ec70822dde330e59cdd7323ee35cc19a6aadf8be81eaba0553cd3ae34d522eb9bb383563e53b722fe2fba91be4f14f42d431d06fc9dd720d5b18b3da8102df526469358d26ab2a2069856f322445f1785ca17c1c3eacd14bd75fbeb468b9ada81d743381e2b980047b40c2b2138057fc249794bfc73ed5d0af7fd94b55a8021aa0fa356d0f2e46bf73d88a23d453eaea059f09fed2b49774a9bb5fb03c9500b010c761fb9d4dcdbe4f084f30b54e15c5cc6f266f19e5aa76ec5626a41075a6da5b2efc215b8ea80d40425b9e898d89a85d47748bfeb06ba2e6af445ea1b90860d5cab66622523dbec77c718ce7e4c63e2d6284720ccd0925e6cb12da80172465e680e843d01330dbe58279ca976ab19c2ad4dbdb26b01feed54debd694e0535a05ed74e42f1f454ac092257eb66fa38fb04e23b4c57656a1da8954c55d278fef5dddce5f6cb0d73b8c4ad442e1e1869edb76e43fbda54ac84ee0869818b12b1b33bf393794d66f091828d63846df9acbf98199f342d7d0c4617220ecbddde477cf08235430534b980bd2f310638a389b45bcd402f800291b9b1ec891144d05f73bb463978b50afcd6c0f20ef94bb653b453dac89909034da0dbad207967ebe36ae45a850d5202e3fc039b3c1c47fb28fdba2fd25dc8f8a3b2a3ae9f4af1958339d65a99bf6bfc138ec39abda2289ee8c9bedcde436a1d509ea188459bbca152444a1c2d14902bc41be020c9430260881c1ffba6d5b42b744a2151648c6762da6f0536222dc1214f608c3bf3110cb5859ed77408974d290b50ebe7e8b0d0eea554e5e63f95d6329edc6d11423ea75750eee866a3605e53a2e17d8f4953ea3c285c4a82c56bd464239c3b66baca873e7d630b8157b78ae73b233a4420c491a59210fadc9331ccd2cbd218393c7910c3ff5bd3fa42fe85f010812b859a416555ae25e3a32058e9a1e9c2155ee588f1fbd6d06e1e6c0d7fda589983e7c0a17bd477fdbfeade0d7010a87c7890cd1ec8e9bbc0cd7ce223372af49b60c6589b0d331444abfe9de66d3e7cf353c21438ee0ea314fc9517693b26823d6fcbcd767bbaa80b30aa69413c8abc3fa44e64a76889074d1e5c865ec8f63d6a279a1dce6e111945d384c8623bed5861206659a1fae3f52f40839efa8f92d2509b66b72f399a7c1e8f609a1c42f973b3dd2448ba64e4b399df69d05abb813b695ffd4dd739cefeff718fba829a87ee9a52638cc410a0a4e9df01321e3392aa4ecff76d8bd56267587d1d28e39dcba2235e8b191d11ab7e33b12d331633706b989443fd341f4fabadacae3ad3ac5b35bf4bad1380aebdedb520adddb27a748742e35a2f05712df6b98158c60b78735b316e4f44aa4fb2e1cd715520aeed966c040f3daee264dd708bfcb034435391aa8e28c93113b0bf01224e6b96cdd5fa80c3a5e0ac439e3f418487be4689a9dff3748efca8bc84ee52ef5eb545434fe94a4e630244f81d3a2b62f5b0b06c6abeb9d2b694ff487cca915aaac9258ba8174d6239ee90ae0c0acfe67f69490ceda7d7417d82521f9b4b32a16c89f435c004c54e7b2869b1c1fb09f8f381d3279edad46fa70082c99019301d715d08da3e13a17a74c97369a7831176b76aecab34c20ce457094a8e376cc2ea095d7ad4fd99bdb284261e2623859425f8245529c0784cf3fa9d2e45288e4e9292b1c264dd4ab69f585e4702bf5677b895ef8c0ba20758475982abc6e2b90cf7cc73dcd104c168e76329b915c8ce0261695e1bbc7a09bcf2925525190d14eafcab45c378ff8cdb4ab1a5d585e2382c1cdce449a05ad7e304e5b8feee3c973a7a4f505d80841a205273457c3a240f450926cdae6d9cef82fda3be20913d6c80e0263aad0ce3905ce707042fc9eac10505f9a3d91b5efcfefb084a72ba0222af28ab74d2e38a8c8bb86748b8a8204deaaa2cc60e5da741c4d302fd372421e30be964e67e3049d8be8144dc911af5688c53ab9718ee841125bdd6323a4695664ddc4f35c6e744888a797e689ef9f50f065bf9dd5eed2f80043360eded1d4fcb79e5cf242348ca8b3d82d4b9ea2cfdd049ef4816cee38272e1c3cbc6c99fabb5e1a4ea1c93a38db7089f82345e3fab9c51b1c57ae5e7bc7856a9d768f4919057b72c3748909b363d20e3914768a781f49e5e20f0a27a47c928c27c95b1be7d7d315c2098bbfd745b59acb6f1b1132e649ecfe1e11f288f05d67497e3a1f3f2f13a538ae5f7af70a51132002ed53efbdca07b4e99f11642da79f034a686e7db54af4c228ed8cca9457cd6c1d62e83cdc095a22274b5e256cbddcabc4b116690b40a4bb18240fa20f31c133892c53da26e034e592d15210febfc4a5cd876b9d6adfc6eb3def18b854ea3eb91d6dc4967287995485df41bf7bcc2898bb8829e86ca3a578718f083c9880f687a48eb389090892b75e6b041ada46f46b59129b54987cc2206fb14a2b7daaca19b9af95bcd7d111174cd8f5c29e3a472127e026c0d42039b82483520e96583bb9d858dc7667b069da21b66602817d855d1bf1a21be68a6aa816434a544888d33fbfb415e796a7cd4113dcbf31eda876f93ac90d52224b03ff6519839d4aeafa9b291c22e07a4fffc3c71f83e8efee4ffaf23d9c55f0d4790294f9b0a2a3b4c00f2df1d1c31567bb248351ca821e9aa5635b75f059f7f1a50b339f266c91315b401953d492ee239c50774e0a34d86e76aedc01c835baef1bc18d4a437119b6fd87a28553774e40a229f4e6236048e3aedd6638120176892ea83c0ff463838f746b70134948ff10704faf205611316f9b2b641bb681dccadf5f445dc9d7f29aba74a698e126a174b9a636940053191320d7f39d74b15813b8e9554443e1a3618a052d74ee687f0fd2a017064ebee136977c7d244007155ea7a6aba29ba11e179a9dc3d1c8b5e0d2efccfcaaad9853740b7b9b5c0a48279e06dae569387a3c9ff182adb1bf8a97a598a24f49fc031bf85d67c5aa67dfa460cbafc1ec460354e7f934d09a4de891b1408e61a9579cfd6fc5dca3bbcef808ad2eb76cc9eb8c781336fb1aa987e75e2017e726a6ae2440c4e573bdfa328ca8d22a7502aa8cd55f2940176187caaba3d3636031261f452537ab7116607b6019197626854310f3506b9d930e7103663c1881506ffbb3dd276ed20f992ae861ca8195260c5c5183cd37bbba7027ac9e982510dd081e3b8df6d0f1c94cfa926932b0535ec4a413c0f64c237ff13911046a2041762b4f7128cdc3f76e7a27423412a47099350cd6841c6fc4604bdf714c5d834629e46afb963a5d5ce48acb1fd6637f7b34700b82fc8dbce67ba8ff7c6fb5f1d2f3aeb5ab3f40466732b9583c12a51652220ee318c357940904790ab9222e125c92c49d170ffe1f1b4a2e122dac8bbad6aad7ef09ee6f03a7cb59c2f879c6cc95c9f73282a57ff596da98ecf7c03f60ce17938a971986d77e1e2417c03b6df7b1517af0411c0fb5422c30d101144078eb6980cd0460963ff22435470b5e9f04a50dddc84edf81edd68a623a0be28d6132ed41bad2b56853b35e91128ce8a468f56a74c0f4cce0b2a2d765acdc1620354c7d7365ad8ffc664974a671b2508fbe4b3dcfeadcc73b20d1252ae3993f767e0e24e121025dd83b6b815ce0a969a28a8de043b576c63d8bc2a9a9fa563c2d268066107d5b1591cc7688c4305afea24ec31ca30005073a8cbc3811eaaca5039adcce2f8747e85267470a18b87064a4ea596c72c2764102990df22932521ef84e49f44247b846cb7bd94ec3bd872c0a6ca46238c55760e464dd2572d8aa2a3ae61b73c1026940f323ef3b7208ebbd2d63820895743c4af3a8ea86d3548a301814e521f59836f357f9e8b13f35a67c6b9fee23a4a42f762729473609b067a1ce54cb45a12d04b4ccf0d5c1fd3f62d2eb78feaea961debd288f20ad700be71fe0170a6c5bea5cf3146077114b6672d112713475333cb53ac27dc5479b5bf4a30b651fde1d4b26d035d2b90d6438795be0e9bb9ed10fa786c1a2d11cfb25e67284760c9d76ca574c5635633ea759ad2c139f1f30b9e52aeaa6d03cd921c5c18d359983f3f3af620538d33b3b92341372ec154b17084ab93710fab810706d000c6797de1b9cdfafc8282ff14d63f58f8516f5d826633e00d630ae3dc95308ca230b3a98a01d5ec726af6505928d9dfe913dac159f0d7ec10b87fc1a57f7d42ded39c0f23082a2565a8838803ad0b003ad8ddec3a048e345b3755a19fad085ac1215aa24a3b4fd3217a44c4495095ba7bc10f40bfa4fa98a1368adbe4faab7ce7ee435ef75195f3b76d753a9428007f4e6785ff3546998cccf662172879cac1b500de6a4d915f733cdfca050c0f2a136d718ba5b10868289c736b9edc05e6a919e7efac97c8a6093f3f54b6efd831ff26e34370e6b41535360dd68c07ef87f781d340eaf9442f66e3c8b01313f93eae8e52498930575e2f3cecb2c876f4e44637575c3d4be3ef833d38850afdb1a0991f0a4053057fba91377e69ef7845064bedd0733b552402d7378c01db1a1b5c66512167d2d675ec137aba6253e57a89da3b915e6305f9726349c967d4fde1b3b2f5349d407ec28a4d67369123f79c693ae6c2154a5b763ed9f27e5f60db1c1eaf92bb7360146de9d7e2a715a4829c1c1b2a5b30a1ffe050e4619d0e23861c7ff48fc4a79ffe7a0e6aaa7456c5ae6b95f3a752e6a19c5831b80ae7d8e7481cbf63a79cd2c65bd8339db40f87bfd77c206773d33d8e8d75ea552f11620509457957ba87f3dc69370e7a55e5a625f3065d93041cf8f7a72040b7a746c32c1dece2917f5fbf7966295f97ea24e86fe5853ce17c21123f81c9755410d723e49e4bc665cbbed8d4042185fdbc3a12e18245217d583019b94b46cc8cf4e8b68d0b91cfe9281e842ee6a8b794014f5d00930959e260dbf7c7fbaa5783eb5ee82f9884c62c7d0e636156fbefe2e031aab7a6f07bbe2fd09aa3ca79ea36a74fc267dc88722b4429558d1e703868b7d71d5e19d20bf878ca1f6c3af26a0fe42ef479f6d2250f17e1875b79341a4966706129d723c68e6e32a795d135b82a7d98e075f662fb0afcbfce6eb3aff8c157b45da3a8d7503194be4f1d8a8f513836874ebddf9fe0d84dd034898401c6057c3140b060a466a874158664349e648604c60c5a1e40f895dfda819946e0f50c6248492c855dde0c3080a21952c666b5a99da88f71908cb4ed89e318549d5fd1f10e27b1279faee2eaada646459476a054dede703867bb5b525daf89eb592143829c123e5f4c81cbca2b8f0e9c2256694150f10bb3b7dd58e71ad0c81756a71a57ec72e09840c73ec0e70cbd2d0150d8366a1fccff532a7661c80c440db977c08f7387c2ab2ebe060a7a105a7424a34c929cfe22c69d9753f4a6b07bfaf18c8630b93ff801492fcf38b3c18bf464a83b2470f2bdc8d33168c70cb5a6a5753e337e03801ade8a7bc87da0ec8cd217592c2733283c3479fd3b7eee7e118685b31f3134add955efc9b77dcbef9fc375f6bb4d448f3360bc93a7065df344a7c277b1e1897d2effa6c4390b929a45d36665b1c28e185834d055accaf8ac8f31531a5543a0d99ee3f9aa2bcdc7195b3dd04411138223a49e95601ee0454982c8e78b74a4aba04ad19757942c89d42f0a7ad00dda8bbc3c9bcacdf2816a013987c228e81b41e168e720723946935027e313c84668ce4801cbe0435a114e1a0120eeaf5cd58fc6e8f747a3f3e78f863f829487c98cb300dc8247e852b326ca0344671ecd6869f872e3705bc3d533b99d448bd7920ff09db3d822943450ba6c72e7ba0db7c0e2429194e4de27415dbfa8b212dda861369aaa1fee54d5a76714da472895928fe4f32b60b828d8094fe411c48dd6ea21189016d6572eecd6088194bcb70f7c70cb807b52c7eb47d70a1b6c77044bcb2341947932a688baf2541c24c5529b1c6a2e70b3c45113d24f3dec5b44a52af71b93f6529d62ae86a94b3f796bf7912a5cb1aa751b36b4185f5b412938f56e7b354c4f57dd7fd8d87bb60091ff58ee6d430e0722cd5b51c923ac796523bc53277ad9e9e544bd5bddf9f36c7e3199e0c5f4e03ca988c5c98b64fe918d21d35c728c6af862eb5fc7453fce89d6dc0f887eed7ceef6b2ce780d53edb974b211bb54abe9f5c8cae44d8c18fa88933336d02189378fec6c56d91b1c57c8a37ea75b77f95170dc56421ccf84391f8f5e68b4102dcd74468c1ee2e1f66e74850addd0f08a77efd296a6d1b1ad39ee8774ed045e6fe459f7c014944577b19184ac5d83a57d68c3eb03b621f2cfb3a62170758d94bc6e8efcb2f3bc7dc83d8b9cfc9b0dc077a0f708f98eb9e442be61582583d4b94d0797cd8612fd455081636671cd8a0cc8921dc9c24fea152acf93195ded7304823f04915731d26c7097b14d7794f69091154b308a02cae615b8814fc4d0f32c6886bad95020517201295b2ea29cd9a904dd11241ea1f1c55e6f08a286651b595ce88aa4660e72a36556c475e8ee43977909a7e0c30654ead33f26bbaec1aba61985e58091df99cae9d12506a67d3c866f4c0dfe664719fa841bf24719104e48d84fa8b3ffa32cfc8550a081e05e523ccbd1632f0ec416a9db5b5b0c323b3c1578eed98463f4ba1d76ef17b5d7a8d041287b1806e69f53909ffc118c3dde5cb4f369ae7736405e4dc4c077d551e7ac6343fdcd2da0c129e00a895f9ddbce00680ca980ec1103ba1fa4c1b89b57936984a1875a8643e1eb8239cbd43bb6bcada79c1b7ac522eaaa034fcc8e0ca8fc983488ea2f30e0880cc6717cae8a7f72f1753d313da5a7827d7ee089efe85c59027721778a7c984cdbfadb2b1e73f17bfb852857c3b3a556294528376ac863f00e39b6766e7a0626cd9974ab2683e5a3e1ff6730d433832909413d1aa69039f902d8ec7b68077857c0fb55bddea7faf47a42c3a96ae29717c8f4113193b725d5dc02b3f4cde50818cc29aa5df64119239ba922df5b7d34f0acc283daf54eb35bc0d46e16dc524157fedf493b85501a7ab739de19b4d88574d2870995208a112aac8daa3426985f49950c3ccfc5a792b0d8a7771de15720f695a62fc95a8fa9105614a696953f063853096e7e60971ff365aa6f34403ff215457a2f07c7797f899b5bfe1b693a0b552b90f1c318b1b1d7353a5c1b1dbf62bc1090b0d25790113d5aab5dcab2f16d69627f518b2c82e81bb8b4e895e832c5c078c80dcf1aa6708618a841e0be8b185a2d3e3a1df90a0b6062dd81b028e89eaaf7a12f6cdcdcbe94d669b6ff675ff66c3bbaa3b809febacfe853fcf0079785fcd3220f8796450e24ee3d7f12e3481282b11c0a4f1737f37bbb388ddf8bf2243aaf4df90b03926b31ecc6d07b50466c70aadb70a211ce2be0c2ee256e88c25e4df7d945eb3128c61fc472320ef4b19f369096183844dce4eaa1c951da1986cf84ee3ff74771b86c45c1597f4baccaedb26ea25b0bd71d0ae314011185b7a0ec049ac03d3e37f4f04d0211fa58f7a2b6b6d7baa734ffdd213c4fe1520520b9902d3bcc8f3570205c62ff9f5d2b991802de6a5587cf36c1eb46d89b45ec9085808c879c089e8342dcdbfeb2ab63395a05493688d19e1e49829b59f72d4b5a3061c7f9e77b7e91bc052f2e39dcbf2a4be947bc4da6140f027eaaeb9ad4a3826dbfb46587d5745f9e529f32efb1ae24425a8c7c39d844d8f02b34025b96c738609fc16d46a769930faf04b77c0df31e2ee564962d5ef200758827b49e0173fb1f7775f21446d89a8b3c79c1d02cefa4ee7657814f3821a2c3201be8e85621805db4866a4689dffbc471048651aa76edf89692c456eb001d868c4c36cf29537ab394a52fb35215d9ce8f83507873b3f0f3dfaa4e7182e7617a26b6968a8ebf4901d487d35b990ba5795f64585d9fa66615697111a4071aacdb130a20f2ea4b7429397c6c6b5214a97e8e1a7590e50bf22220799e7404e6705fcb5691b79db370ded54e8a83f3a0ab02132bbcf808efc64cfff0ac50c49cb9e818600ecca5675beba85e0d2840248d3d58db3723aebbb54c1e4e12718fc82f6beeb346bef6188f1d18d76869fe5e8d706f5f1acbec8bd1d6c2362741a4450ca2a622038ddea53f4bdc284c79785e47a29ed22abf4e72c859e749795e6cd000a8432c961fae350f4203c90cad14e657e0bdbff16645769ed684d336ecbc46a18fa176e9b4b917f651aaa3aa50781c36afc43444a8ecea5fd50a550f28064e948e3c5a04356fcf072166e6a210920cf5ebb5a643385b932c6e23220c119f3c04058579ed7fc5fdf6f16449e2c2212e45a97d18ebfdcb06908adcba44cad40476ed637c6d77a39b7d519db40b8c5394aa91034e06314515d410df7f2d04bc21028ccc6d3584b18d9abe39c57061a0098f4408976a4570f3434df88fafd4b91c7987ebc7862879c4ee9b6b6661e942c3a0291ee9a20c194b8dd7a1c9a3cb0d85062343f7fb23d195655929a07b11b70fe5181325a60992fea1f7de4e6678000b3c544ced5674d8e20245fabb7aed853013a7d8004313ed309e3dd29b620edb92435e033417e6c1a375368deb6c9e10d3a1109883f8aca633253268a0c5492e5ace981b96188807f02c50b3e09a4e0b671eded15038bb4bf49c95b29ca5ac47520c3c4d65341ab38c0eecd332676318b43882443f52105baf8c843be89768f76bd054072306307dad721dee44e6121d449bce5cabda37cb767291f4f3620a9a1be358b2521c6a2a9434853c413519dd1adb61b9aefb89aa1baf8a9d68f5db333bff5946ee4727f1bd1e839f145f4a7ea5ea3497b76406db67e52ffaa60467280a3a61bff4e8f905f7a4c57256e78532ec2638af629458c0362143717b904900a6a84fe8b864afe7035bbef4a16ab6f4dc85f4aadc109edad14daf399fcbb9fb71bcf7cf47b3544d0f779d19be5df07e449e1ee1d56f84613160332d5bc89fd9992563ae446e800c4d30f678bbd0802dfcb936c35b59ce9c387a32518a222f08cb89a90da9060afe17f603a73ee0d7822d05dd16cd8eb307bd302d23cf9594a1371e6165e26b3e96102c6b144d0602f2f552096d9e46983787a7dfc1eb9047e0e6b8674d5c8b7d160a7b230b18837df6bd64cc24a41bb6d5e0d8e351158523534c25aff36980373a0d5d2ac3c73cea9953b7929f123ac721b805a1963ad97a2a8f2a96037991de4dfc9f47abf49e1a13063180ab162bc1f74dbe370acceefd9aac49e413f949a3be708988192931f08601074699a7eebd494b180276d25d40ec561526884b2ddb8d178ad5013cedc273c165b2da507390b1d1a2b03ff4fccc788bca4e4b99d4e43fea92e84e83fed5df08da0e7e3b255fbdc91b857a19fc2893a0ded5706b0b436a57b8a351f9bc4c3a0fda6dc021d92e369e74b75f3584deccd0cbc27b1adb90798c00f03b9f4d23f077e1a76957e13940f9fb155d8a19ec53168b12f246d9cc9fdd7c5edae038da28e843f4a9ed2649fd860bc7d5e69e82f02141348c9e98c31913795575163a326a1103b828a4ba3e01091aab601d32f5c860c9cabbc03d336f132900900b65fd07292024d7e5450d445854085a09947c2fe4a5eaaf207d8d0598d1507c21baba50c3f7c765be200539960dbc05953e350897687029696ffc3bd90d7ca056128fea1f623e49cff3633dc6905c93407853f54f36f45765c1ff80cd2a75cfea207e5b7e9cf2ef370dbcaf71b76ee3d05ed90fc9f743edc340c105fac96369c30ff451e71d9253dacc30ddd3b561b96fc94bdf29a5d494bdf89be963702eea9b222ef092fb08b5d0af4144cf1ffde90befc5ce97e8bad2a10e22e32ea8567f7fa9b874c2aeead3f4c639074dbc9a268f8e1d368c5319f4d5aea6f3f88318681913b65202e4e71c0544cc1ad2f4c7ad5781aaca97a2e5ee8256ce72be2cddd86c9bb63296e1ceecb556e9dc0cc839f7ece87cc17d4c2a503a0ca0523c55aac24a1fe96d3ffe94e7f7a3c4c44e52c3155bb670ae5ebdbf1b4edfea5d5fb9a98a312ac3e8bc8266153a91fed42dd0e918504524954def107bb9c7fe4e45e313936ccaa34b475bd3057438c79d3720a174a06364ac364eb0d3ee6c1c3cc74d233e1de8ffb9bf3e7f00853de931d570424b81bdd34c6b92cd306adf736cfdcfd38c72de5595713154a97bada7049d23c1ea6ba165d995c20e3da0eaff6e090111ea6eb7018dd1d2788848ae41f11e85085e57f05498776b83dc05dfb4fc1684e7b425c5fdfab473657f614fa6eae226d7736d3752b3a3c2f7d8a82b48453d9465eba44819bdb114b3e1d72abbeeba8097463ac580f1ecf73a311c1894a8a6c7b7d4a9344b977f1926e799c1e531d9f15eded970219b1b8b7d14cea1b49459f734d749561f3076948ebe53b59c1172a3d9db5f24eb98a3af7f1a00eb00ab0f19951df3eb1db4b9727e0079f22830f6ddf798f170ca9dc01a3f4e93a8fce39025f423f6ef96cf9f659f9500121224c9fcce2cb03e212f4fe0288344d758842d053e1a217e0e8936e1ebf02f139cdf30f1be40946e92af5ad2caf5b9be69840bcbc0a21c5d9167bd516c73435cadbe11b395f2237f67dd14f0b33fed32f27d305904611a57373226cd95a1d826058b368d76750bdf0d0b230c96dcd99fd7f3bbd40dafa04a4ab429aa80e9f5eac8f2c6b2246bd91d188cf2e32aa8239307ff52a2cf383ef243a73a230d1491e2ad8de10def8ede2b6b456f3b44ef1c26c8cb73b8025ed8b8df21316a834be6ac0e5d130bdb0fd922d703fe1fe91e90586e8ed62c43fccdda5c4fd728dfc2848e8c4b47e52434d6067fd091e9f41c4efe1d832183805a0aa306181d3f4c805c3739e163567d0d9b9d06a1b3a616cad5edf2b42f36d186f2dac009938b7d56bb49e6c18cf8d97c130474aaf9287c915dffb72afff1161b7595768104dc8d71f535bffcd6de2d036771c410d14318d0708c4c420594f8f1dba289728dce5b6c02416c6ef18aec01ce6fdd00c0e83c0bac6f44e2074f7ff7ff84428f0b5d68033b1d79eb1782de76eb9b5890e0c0bafbecdfbe4a97b7bb281b1ecb7c22c2c4451933c41c83a86de22bdf3d98083dfe6e4d279bc82fcf941529df28ccd66b4a3965ed9172066cf410f058e73f5c7bf6ef494434418b92032b41e87b5a3dec3f78fb87363436e418719adc230467b412720c7a5b7f464ca5ae8ff37fada72507fdb135b5283f69c258d5aff55f7f48b56f43eb9a1d7bf321d40eff376ca1efe3fa396ab659d5804aebf923a7fa55c92bb3161d11dfe34e7c82a601af3545465b0506e95d21fc3717fd94376a604507be28c01c106cb4c2700db82292ee72c11de7446f7aceeb5450ed28025ec33a11f05ce9cff254db3482ea54ff4d15047791270c2163989382d3b732647238c8457c1e9e78ca0c41dd93b2a88231e1171427dfb4dd6ede3c0e45eb136c3f8312101b4a89e4ee5760f1ac84a374dd0817d9a8f75f03659cd1e9d8e721d1d7301e9fabb5428332df8bb74afaa9a1a0451b7d0154e292296a01fb2eee5b806ce0631b3a0bb05e063a07884ff52f1a5d109714719fa684daab195fc841641a4098bc9e94c4225d0927cb6d44ace70fcdd5ae42460ed05a0fc0e69b97ac03730074e8b53473afdab2e04fbec03584d81f37ad1758360cc7f8b9543babf2303c70ff05ae4ee3b6e2dd9f49bdbe33f3973c98b652823ec73950b0c26d69f64d417219dca890aad302f555cf9a9bf1242bf74f5b56fdf694c3740b1bdaa258e5a5f61ca6c8cc997917e646f9bed9a6859aa05e9103af999119bf6b6f2d18cbce888c166acfc87ae5ade35bb9a76b29aaa2b1a6315c922c4a1db84c99f20218c4fa5e0d7a740fd6cc3406dc85f34fbfa66eaf03fd4a6d93368ea6c048760a89e9921fc578abf5182cb71fad1183683d07a8a35cda4c014c1757e05c47a489e08b67923824978c16f34531ae6bb452241e89cad061945573b90a7486ce29d97accbe1cc43d335b3fa9d95917e27d9149f3287538b070428992e563303a177c3346c4277dd0f1dd3090dbf470f8f6514e20b3859827d7c4ff64614fb51c9f48bf60880a31b67eba59c08f1391848c2c6a28f9b3dbea948b51c47f105545731dc87a7f5b02ecaa657d3c6d971d61db3b3c1998c40219b0bf11a84aedf563c0201ea6854a25a6bf5c07e838a7d15a151a14764a20da71d5d1305619cb9748b1cb4cc622fd18ba564bc26961c5dfe064fa15f2c5112046310680a0056139cbbde3246a28dbd0df499f71feed8f4f3dfc97a19b85215d34b49218a5b5c4ae8bfd2fba69a43144ee2b2822790f26ae0d9582b19b360becd95745be5c70b63087ec0c08a7528641f7da1d196490652e7ee7c5ede74418ebb11ca9313178d647791b5ea0060b0aa2d292c4fb144623e7ffa9131e4b045226c5f9f053c3c2fa35de0504bf7804009d257046d4a8b5764e1d6a339c0f0ef3334d00c81796dc29109a67350cb2d25e1102a0e2c76635b1bb0299385d1add31639800eb6ccccbbf65b991f34c0478fdd3632d54d863d72eb2eae1bbbaeef54ec400d6421517e4a69345e23cb156ff3f07bad8dbca5d7b7f3b1d50332fc85193bad242c1cfe90c095ac836e04f34594de8b256f8ea76c7520d136ada1c26d6340a8d4dd74ceea5be2f89e08560811ffa6d38525f872a47159e565cb476d895e13f9e3cb1562264d13145e4b15ec7d925249abbbbb086e41a5271e89a55e055d1daf9b701c1485bd0cb2b314d4660696ef32ef62e0674b86b07ce5223368cb92cb2afbdb9c133414b55db93931c556ba5245556790f2f3fb172be3470dfe552c70c44e2a7707266ea19a9359c7443f007eafeae860e2cf63b7f22520ea94281d505a6e1b88f1e9862d6a4a1f1930f4eea276bcb14058b6f8e9653872d4530a185eea6ff3c90e78a78e7125ac0ca11b8640b7840d8fca7de1636c5ac88aa252f93cd8a1e8cccc5e720755d445752d723475fe47e941a8e46972287527c672dd7f6c6b52ad8f9835d28ee81b1e87e2ef0e9dc23fe80a6d49dc2103f8038cd324451dd6bd61b26c6245a61f24b203faec5c5d0f860f6a01f3f31bae77685c67620fe774ed0fb8a14a0d698bc76b7d77a80b3f8df93aff7198345f3a9c28a61b282f3e59200c423a047ba37c1ed82bd1d7d67d81c6a5b890d696a51355b2104537e5868303f5e2585990a4387a0f1141b6aa7e376e0394b8d88a88284dd92cf819ca81bff9a0713cfd9f4e4d7e196125f7512b6e6b49b572be3c4291b96d24f42bc10d3bdeb6db1d230fd2c7772551b4310f552df1d99e2f887c9260d3e2571a80f7be201d5870f6e6ce8d2ceb32277d1216a65ff71ecfeadad37020b00a332535425b193c685deeb94c988de7ed0e8c4c7c62362f57d6b25ca770eae99063294c28be7ac223c4f4adff317133c6749b7777a174c9a5af907445499ca9e21ce26a5ac3fb96fc68f00de07f776b64c6d9c27736b2da1c9d9ad3378a3bc01989c2352492f498102ed394be680708cbf84d1eee60311032a79217f8cb547066056a9e17ad9fca86a98b49673d7f336a379b8da24fe3415981ea871ef1b3464960446878718cc9160077aa57a0756df8afc10ccaa70e5ce2d3cd380e0a023bcc592e3a2431d1893f7affd051ccc5cf68f59bbfcc6a468345a073f0542875bc1c9cbda10a56588295dc29ce67835f7d7c5ed9b7e777edcacc5389d21bab8059c904f14eee3a98c4aa6e15bd7a82968bcf89ccf754e4ff880bda884baa4a652c840430c4a1ed1cfae4b96fcba4150e429a94f907483f61ddbf33f6c481af84cb8313fb6adb7dc280bb5be4a2cafb8b1ba09b2cc9f5ccc621b9130cd1d0f8ba060b503629410c29282de41da4164c132e867d4abd0c267b2f576860de0bc3a0fb244a536ab8177155398dfea54717845a7925d4ae0f1f3651c427ea65719c81056313b1c8b993197b97908bbf899e55aa179b6c607e690b0642f9a03aae20b26187b1796a200398dcab173e15a206ce4aa434182f9e5731137299a9fa9bcc709dd24078e05b436fff39162f1fdf81c2a237cbdf4ce12b37ea22ebef61ef2ab556cc8869a7810f2dabcf471dbc34e943bd3496bf1475fe517df1cfc443170df400a05557c556917585baa1d20f6ffaea162c14f194e9d1285276667a8206881a5f2817bad556cd4ba798a938a2489a6d09b902b1ed2673ced58e28fd69f72204491257360efb8130959245cb4025cd4b04f6fafb1fce1c8eb1ab6f078070182ef3ede34e40af3c3a90e039d3ba994e7bb3a795549b28e25bcb713748fd486fcaca34812941aca336b4aee2b835332bd1a668a05c902e6560286cf05f1b45848047f26be3c96dc84c7fa2b8c84552502de03b6809e4bd9635a0d9f9fcf951590b303ffb10e2253d7761e770080b9b8924382c3e794d206818075d2202571aadb73d4d52958881aa8ac01fca016fe0cde67ff11eea8ec0c8c9f787a3010a2b5415dc8c3998b731c4230b843d88db621f1a11918ba915fd3d3941766b628805771fcd5eae3d1e37ea858f2f20aaedcc7837771216e21465b89540a364953edb4df84edfbfdeb86cdcc3d1d368928fba560e269541d691236f0571656a6dbb97d47362a1c8f0d5b0c296b6236aaa866cd3671ff48c2a7da17944abd148ac76965124062b11911b15f1937017d7634ed9e0f5588a4ae1b48056823ecb3857d1875126e9788112ef8c83928306ef278293087948800dcc47debddf745e768bf35b83d2543a2f274972a8bdde4ea7d1619959357df545e01b7b3dce3216416f942e71bf474eb2d2afc60b43d39b694e682c9540f4a4f068fac3d7a8faa729ed313dff390a541ea12e3cd7f36f8fe36b0602d5072c84e360525755217998855a65f9fdb74e83985c905c69f42bba13eef3f79519fd8cd478e20c0226cc419884f73c2f8609c1cfc410fa06bcc1bf536a1c04440403584088ad4050d3a9e4f62c41b3714503d67b62d815981cde7c5087183778c2a1b12103584d040799e6943c8ff6f926a31616083d3ed30a3f912ca3c900966079af240183c6ccd281555f5dd530906a82fe20f578c5bc3393bdd59e9d79b7dd6a52f038fa9c7872ab96d6270d7f0278ef00cf068f7407aac3e2498b9d43756c7f54fc9d63e5ca57669609265923b4a4d024073683cdf743bd0587093180e1a7e05d888bde1bc2d163d4ac072d6e05d9a12fb1ba3b8cf9c1d945b7396ee296134c40caab074f8cab07d97fe6bec774dd624902dc650f1e75f150081378c670124b8a667dca071a3f2baa3ab1fb587751f76bef4d039e462a067dab23d51b39fc7a4e97f085a03d42fdb67fa5564ae7aa8178839a87fbd58794c9f89018cb782044d1f0fd7a23fa5cef8cdf9e9fdbf7bef2201e4fb8a4d4af5ed65e153f56b1a45902d0ef3153a534d72d332cda510cace761f33c9dc9e00f2c340f85d88a7b7a0146e82488a8a771fdeb78ac6548fd5d15d361a5bd9e05315a86ec1dc70e7f31eabd45873216e9c4f0b3840dbb2c8a52c337ac165d44f961c0d5c45e17d8a419508fd405a048c12e58e2f9f16810ccac63295ffbb3f10d47820ba6dabf3168d12db536871da5f0250a9166f4007f642bb5ffbcd93a0d5fcfb2fcb658697bbf4001caf1cff9b7aef638bae70d1b3ad4c36112a3548ed1b627b91cda676381fec457f3c3e2223ee08bd87e96eac1c8acb1d6ad30d249bcee18878d762a53c36c78a1ff875cf9f00a5221059ba686165982aea062dcb921340249aa259c2227d3afb3811b564f06175f64061e2ce596b3c2c61a7bbc5d8c54681fd1b0d7fd48e7151416f3f192eb7519cb56caf258fbaa8f9d182946cb2572b1554724bbb9e569a9f0a955e2e92e5240e20a1031b215c4e88bae01991eab4247d7f2a54ffc3623a06d922b4b6d7cc499210c7e544be95e69d4fc3ab026fcd52835e3ec6395a30776dd5add48d17e7522e7ea1412310a422b336f95e73ff9604b7b5955d741e20e5207abcc96692b72719b04d3d1a34047ce4f8ed81d5208e2bc040eaf2b105fbc1ca89042e73296c6b29e45ff7b2f92686c80c22f9455846d67c3a69f727ce1571f648c5c588590cdfb032de18545dbba4245b65e50fdf71ab6b2cb9883516284385d8f827bdd6ed272bdd7b1ed9d34e636f1920d3313f48b41ee5ea18a4a623ab6eb7f417ba39bc17b1a708ab7fdcae3ffaf08d1250a801ae1b937ba9169ed91047202683057e0cfeae9619c33ec39efce4997d1c7954a48c6b0b01ddf2cb910f9684497b0eadd719a90b5f03e42002e65643cec7f81e0fc1fca122e9ffb127eee0a3945d634db46f817572f85be976672ccc0ac0e83be31393d822b5f12925ee683d407561dc14c5edbee3191b8fe5b840ba364efc6d2ef8019c83c936854c03b088e5004cc6496ac0d30130d233febaf4f979f9bc18ac3812ed7038ecbf0385bc699d88c85eedbd34af4a1158c33087654633ff2732db069944f041015147fa67594df4c655d2ffea2ee914d7a3b51ac32a442fb3953c986a627698296ded5a7f730b4123c36eb5a410a440191188db6c6283a2d43ca7d39645df1159e24664308ccd006871c0d2e76d704737d2f57eda9e33a4f22006d366e4d15e3e8f811d184057facd5297651298a8ed5bebedf3687e4fbb730a5835f2f581a80a156035d5e36ceda99a5c325956031d29ae65a4dbc5c8ad904d9ca5e354dd49c085c8730d48e779522d949bc8930604cfe7a90613a762c909b777be001a08c7650a53822a370ea8860566adaa28dbf09b119aa24b20f1d33d20c8f0e46cb4f547b5baea7f8a2b3149e3128012707549c8481b756dc92f7a3a354f189756bdf3adb0ff447ecb12b9769d057d41549b5c1b017341ae7fb0ad414b9443e12e035a7145701d0fe40cfe855a10a0a245c7926ba93173c7589bdb8d2fea9fb65a7263827134c22022189742d45ee22a2747e9a42621b92a6f16317450f08ae999275756e6d839b5c766bb578f5a2631a236db1ae2ed6729966ed3a133bfbbaffa5006757d9965cf5656735091e8db5534995e1d652d30b3ddf4cde8fb3893f5253385848e6573a5f16611ea1fc6837db1875add2d98759398e292aeff5928d8616d884981c75e32cc091083e6e54ee151c24d8aeaae76ad2269cf640c1b22f15df3b82a196c3a8ee414bd108e76f09d238768c026b3165be100b4391555a2dd41451ae3994d10ae7a3c7677fb5e7572d884f2f56f3b82edef3a097829f0b5d5c0ae7c37f2b419675c62c04ef3b673a6756cf7434950d8220bfaf5d32247a0bf460127f4587b6b344bd48567f18e8fdb3da08fce05b46bd52812d77d7b3660a1e8df6bbd459f8f5278fa0a983caaa6fe4424e9fb5ad45b02c2f6aca1e4419d1894e04a0e32e5e445c71b9c8380d187c365a6aef1d8a09fe51f8863f5dc96c590e9dc9419ba14f586912c5f2a73983ad35d609d77adf6fb9e11298548950b49f28cc977345bebe36ccd856d58aca6647d62920aa49b6518bba413b6129df36b45959b1d0dbbb6127331e7b3aa80a2338ffef550d4c16b354caacaae3cef8a8709e933bc17f10db0bcdb9d93e4cf6eaec69c6d89a01c50f37b30618d66f753fccd4a995928240711742ad60e207c195a0bb530fbc0999cef04bbfe0fd7063fcf2b4acfc463d62a6f012de27081ae2d5a6b3c3d2d04a5d8de363634d34a2c33d72a22c28b03f64648f59f77e0f1b333da1b46185b20a09f783935a32fa90177858298fec645e764e3a92b691d32dea13599cd2ea58cee5e9151a27c32cd165cf8b14becb350e1b35385c88b7bdde0d1880a9f06796eb13753c6cc1573a749ea4323a870ec4a8c18ce02c00f47fee46bdd42079e7c5221e9b2d40b94acd6a8a175f21c6b7e1f233b6767b173e59057aa70af0c9bbc5e950854ab8db569d0a405afc4cbc281c79ac605494e0a27e013aa906ee93bbdbe0320dbd66ce0f5d9b74caf71a551fcdf5c1bd10673bbf56ea4f1d130c2deabf6ce47c7ba4706bf58f416c805a4b01d9fba05891c1627795d45a0065af48060b73b225ed24c2f691b4f924ed9f3db16e7515c2597ff1445c6362c5367f44d1b80d5435a933b31719f4f1e5ad4cb787fb403475a2888656416f60de3ec34092f36a4adffab8b6691b7216528baa778f1bfa7b470243ceefd088a0e9b17cca9377ff3e3795eef0c4277e362ff3c26f6ab7e81b7eb2c8febaa16053321ce2d31f87bae2d9a42b203c8c8aa61162e7ac96b3c5de10da9f00be26d3f47862e171a125e6a457b2220beb9250cbd35715be73d2983c57075e48d562973557af78c2c4900283d9b4e517a60bef87a39537c7b91a01899c2a945ed3145fdaf038c237834f42427598c0a95c3c74b567c1662b5f549b0090b60c4692dc7e5619efbcb0d8edcfa8d17f24a8afdf4f30ec3a6745d4e45e6d11bf3fe1ad538764c6accb15ac92ed4734622868a3f44388bc91b04d12891d882939d385b684a1e3aecc73071e726206a44597b2295a66468b53dea603e0116575b8f225684aca2923758363e022297bb60ea490f33ab579898d12ed701c2d4c0a46168b5e058c7eb698ee89cdbb9a3729fc937a2218ab67cafe4467ac1eb4be78b7d1deea04643bc8a34284f28643629ce557410533bbad76ea8aceb193bf82526df2c5c22e996c55db96e1e5dbf7334d9870c600ba92352f79a43fe481702dbe696ee00d66acc4af7604f78bda9842b4a8bf2c9ce374ff43637ae59c07007aafc4c03e6e0163a9b275e1fed1340d212bea74fd5b5c843d32a355059a305bb9cb20e7c43e821f25b516e6caf0ca81738c55778aa75ab36457cbb65a59179858cd74e3a82050aadc3dfdf95606369a4771118223da2389179a2509a768122ab92896df6228b24d635106733c07db493f1c978d1ed6e8266400c472aa5056a2c15cb1288e45f18c97b2dd46757bfdc372fd313ec847c6a658b81eeb64d24ed8134df4e052ff5617c7b74d6f2408428026e44ee7e0cd3b6bcd2fdcd4d61b89372b261598f499ad2d3ffe0e3bff101db95a9ea7573f0893dddfc05e22f2f42517dd538e62a709fbbc9d603858e68e36f6340b09a61225e3ce9e682f188a27a838612bbc0845f45db09a8af5fe6170a610836c22970e8a97cddb63565bf9d203cb6d1c2e1ea5114a3f07f042bd7124109f4e2fa5b4a4cc86b681b08f379deda448e5d2bb67ff102439b8ae1b1de0839e47ba70edddb7947d01628334695c958e118620aff9262d6aaa574dbf5c3b9138b845fbd68770bd6585e08a170b3aaf9917f518e4e3cdc5eeb0caf11cea4a18cd693f9e509991f938ad8cd9a593780ea0a04b9ea639fe67bcbd2d117d948fab6946978fde46597141d7dc9247ffcd104eafb506266797902a84d85b83ee8773ac46d21f0ec2dc70148fd43e6d62ef01c4197b85400b16bc293cd6c62ac9682155285bdde8c337d28f4217a31a6944b02e84a5d286d5f3f4268eb16e2447b277db3bb0fbc98dc34c3d56a79577f7aff40f88530e43ce60e8db3355cc1b9f3b30391510db90b52bd710daae3dcff6436215eef82b0e6d01df945b2b5e070f0e17f37cbbb7f4004422d6ee936f762beab7885a446000f7ad138a526976f52492245b9652069372dbcc21c1e6f1567ecbf517cfa5273f2efb6ffc77e5127ac00d86a29f809eac01ea09b163f3d3053b0242c6c374c64b88e967881892c7d7a422174bc9604f7229723a910b4a095fb154ff285e5d256c975e3bea3bcf8a81785a9c932c5d5647cd888c5f446e91315ad9e11f49cbd8b9e536ee167105e1b65c1a3b05c3556e369743731824975ebebf0a5778207c54469ed5177e46f3f58f0a047171c7e91af14ba4518ef402b62bc666d2dbd56edec816ef1d224a33a5156eb7affb4eb9fc883d115b51baa0a430b0752d8feed37a0228773cb6de09cb75ba856976e6a93465be65316a752d4aeb8e2e577ee72f8d82bb26d3b3cd79f687a388cb7878c8f00b82eadb7f9a1f20d12ea01e5129438f6fd3438f96a635f0c17a30d13134a3d4f1299f5a0fb5c067449eac67e1ea8834f0555419ef16cca0f8fe1ecb46d8fcc658e6beb68a0a0921583b735867d90838d2726d9a4330105b69c6e721b5d6060f46caaae1999713e05bf7a66c85e39938d07a85e7f79525753a2cc5eb1c57eeaece5c0713f40655173e955cad6fc003111cfbaa184545f99cc5ac035bb62e688df8287a5a3ced50e58df138ab7c5607afc3d51115c099109f4e2ccfb7fd52c5ec1337e78be99a822adf033870a18650f09a1333c00dfb0b3a6e108f2ae02b50ed4616ece5317006f27d9c0730e7e8bb10e0eac01ab755958a6d760313b3b14251e7a199aad388a3cc780c7cf81bc46db66bb3c8379fbfb152688be925471d09d0f7b056932df4592c5d715ed866c2fa86f585b1218b08a16102d6e1007c4d5eec9b702174da0af533f948fc16e6eb4f2d20f75e7bd6098f49d8f64267eca1276cf5d2070d7d451657854aa7c3793f1bf1d780d53d0d1b0d492ad3dfa7f609ea378b8178f979ffa452b1aa0a41ae1dde21de3e75977b0388ef6b2892531dc63212227daa7415fa46aba8cc9408268a15f5974e44527f228cc8efda8b3f9eee2bf13f886bc40bb44f11044f19c128ef754b0f055b3b85a08e995b05193d4675dec8cbd64182b50564a4f2c5ce9f84c7290dda302551b9d2886c9de201c3cfbfce332d10f886caeb77aab02510aa084a19f4ae09a87e2c93fe1aedf2f8cd20de66849c9f91fdbac705f32dd0d43187fc54c6a30bde27d0c206438bf3be8dace65fe44e99954eda72dc4e3172973e9441e7c893390daed7dfcff15165b4380e4a6481fbaaa5a3175301ca2057297d488c0615e4ab8742453a3f5e0addd9586bb153052ea5d762da6666d9f3de424dbcacb17ec8d086cc7dcce3d3c270ef19a137c0394bf173b54adb13e094e728b8dff19d7581979f060561017fb327e953b4c3973c50fb2b0108a550e7859fbb6aefdbbac7e3f30336e20e69f81ac90ad42f264cf94a357d3b3ac5032c2dadceba9772cf67bf3e78d7c923fa7c80350ace92531de7f74761d79d8ccaff103232400220dcd5c008cac9a96e207bb2e0689bf73c29528807f8d17c471b730a940f1b1512b96e0e5b7d470ae8372c55117dbac63484d2e42b573e74d8dcaa85f1771bdb41516ed72db8ffacfe3e3ed2aa4adb0c8eac3fa3e54b0e1d0d2f069cc0810fda11572f17fe41e20466889cb4ff6c5072dcf8fc6a712e36a9a8fd2d2a979896d08da2e2fabd76901ba5cd925dfc70d054f875f25e57d95dffabf2bdf1c39a766e8ced5b83f3d259f712f06ef69c6354dc656f8fe883e032d4eb8791d6ed67ed00b5382c18ea90c6822f636307f456d397cfdd790d8846bbaa988a55509e4822d182546e2e46d04d86f5b98090dcd60b05d86d851051745317dbdf2b2b9b2aad6821fb04341304e77f47a7582837310de8bc9556bac040e046ff803c22791d5d30269f15fe29722861a5c79aa15b82686f427e49b5d3998a734e3cbb8841d1e1c5a9aa2914b097b05fc5383b9c97c3c05f9751cafd061cc26590804622d7616557337b8ec12348144a0a0d602228df44fa9d2da31e1913b35aa1de739746d2ef1e7dc41d2f0039e765354c804be2a8c0dfb70902465f2924366ccf734fa5e2ffcff5f0ecde3ef62989cbae440d80fd6f29182cbb3811bb03e227e84a68b0463ac20a0b1347eb831b0c12478ee417447fe85d8d62d527f6f86a1ce1a4c849e7df281e3b6646f6374835e7ebff3ba3d04e0fe02506202f288b0ce84704abd43dc6e733880cfea82858bb2d4da3faf82f1fc6f55485a6d4f8479767aeadcad438300df08ed6d5099b499608ffced7cec946a56bccd882d1465b5e63a1b79237b988e86210a4d00646b37bbbd073d8d8929062099af9891d09e45c4671025e5d61f2f674b71789aa0072366a73e58f9dec0146272807aebfaeb61dbea9efb0173e748bf1c9c9bf6f4e4d4706b2d24a709f1c2658ba89b3c770c37f8c59ce90f189cf4defd86cb1eb7b90305f5df440f4ea6a7e36d0354e80a52d991f2e999763259bb2f506c7943c2bca1a6649f4cde28abdb76ee2d8b3f599e74de065d6e7fdb692bd5e4efaa92fb158722736d51994e29a09fd9027899ea41bc7a356282f650237e835dc770c33e36db6f6f6e8e09220710a18c97871aab84298afe14d4c9db323f47773eccbe8cfefa747cc16c31c92f5f03ac313c2ed27fffdbd8f21cb0611c2b3f482ade9ae87abe60a7436b9b395cd687675e980957baae0c6fe7b5db8f6c107954b3324e243b06910a27c0a72b755a3a26a0431f98411569cfa8151a1920b004879f8cd0d92d57dd3f93f9d7c28f4974286289fbf149b282540607dc90e3c839d8db1f6d342ceacfe12861afd1bdd8989df69e06cb3a58953a252e29005df0bb86fa74115696bc2f9a456ce7692982a5997f93a56ea9417804458d10138b5d7c0a722fb3a2fa2e85a531db279f035801b37bc90039bc03d11f8b232e718261ea6d1d722cabfd085c2cb72081d5fc7e237e0a46f6785393890902bf916c46b4d203641af6aea3df20d3b1f0f298671437f54036ed621ed39bd5c3a4232b72fb59dbe96ada1d20f607e9362b11233c84b88e13187bd427ae9fae05494ded63504b69d5d903f1db599c8c5e4717952391d451d153a6862773bc0962aba4dc70ab2d2a4f4ab29eac0487090e0c05276d38eda9e0d0baf2f6f37fad8ccc460a1ab49152437c25cd54d3d233a246f01b8db52e002a2d5cc21a70476254cafe1db94c76de2340ae4eb68427625341977196d0b1605d116e618827b31f619345e7495b51ab5e0269019e658a6cbd475560c533ca501bb129d8aedfd325c9638db7c735574f0c4573203aefc95214dd85334f927399dbac00dced3dc2cb555fa32f7c0d6c6f8c34b1e82c9a5a3e365e3b04008b50921ff057736aa9e9a7ff13b6a9f0b9e0f868dbda9645770d05fe33c2b42e473278c6744383ae6b62768cf93c7eefe4c95433fd1cf59b944f700294c833d7caa5e23e10cbc87d87e9aaaa5925123baf341c991f073e3e417b4380f47f7e279b5a68e3edbe765d6669a887cb36b8f555104f437f9ebf54f4271b211514ef0ba4af25e66cf51d3c56b14a38b1ed0217ea400c25e854952956c3a59e2323755cffbd04d0f2714e33ae6ba6241e6f4c68d3d279223d2ac3ff253fb09586c5c1001ce4c283461b5f47e2082ace85ae192c1ab6adc6c76bf0eb6b17e3f2182984e28eb94a39ce97e279822816dc8da706786b9ab9cd6fb2bb4cc6e010f2d3d58f0dca9996a4e5c30019ef69147c445198f159b517792ed435e10078a50ed6719f2bb222f0ffe7f9fbbc61e07e576595e22bd509af37d9ca6638d12bbfe52006269d1d48fc610ffade9cae8912853ef606d9663317cfaa6ea74f10816c89d2e4eb0f55402d15cfcbc60155eb325f945970a6f1cfd12c15985e5dd7862b145b4684ca1f2f9d19a4556fd4c28deba3afe142bc3be0cfcccfb671b8207e413ec494105e2a6cf243e2176ea727d230a635e37fe7d638a1c82fac59c8f668b1d8858fb810b91934a46b2d4b00edb706163852edd6ab0de9c1b33f1a6ca28427e1e89ca4cf16f1c9af0bb62ecc5b269b8b81abc87a43668bf3773a12d4b1b6df053d54bee33ed15d170ecf2657f67e199a84858f18ede3ebb603610e45fc78de5d8b28d9a30654123f6af97a08792e66db81a78711dcf90cfee5836afd1ff5c0943c6b1c9fcb6e9068cab69c4f1e88beecc38c953058ec98355d9025af47b8c8512ec5bceb5af71b8da5dabce8d9e878375e6a7baba09d6dfa8e9774ef2cfb1410fc48bb17c8213331409cf3a12972cdc6a810ced085395051eb97227df881921b28f0c9234f499189f6235fc193853387c1845a4016545cef01675af14e38ee534e7c78a54f88b0e43be17c1296651341742812c58c760902804a52057f184f27709d94b7a08bc98d27e8a2c4e2860335bca42cd59553f63a248a2631fc44ff3fce138dec37a2a1f214d621c5ccd36c018c757f8ff0b8fcd2c5d9855551abe6b4ebdb7ea48caa5ecfea2a651afb2daa01c421f344c396d83873d7c4916e5b05cb81bf9682d26c1cba2b89a2aa6b1f25fbdb74cd44f5a6ef1e2346d113cdca04427050d5658b3a1f512bb75b96fef661ebd1718486a2a7e8b2802cd2466fd8a9438a0d4664ce9a5a8c8d2e676f170c0705ef114684a8082bc4d5b48592f51da573b56b04b1e92fb2c611f3eccc8f584d96c96b9fa1683ba4e4bf32de476ee085b8c9d2b19ffbaec129269a44f602c11052c0a026175ff05cf28608a7cc600ddb84c389d6286a00636653f76d247584cbb655a1412b94e3534f3683ce9345a3c00cba1b1df054c4e04f09554db84c39185b6ac50c58fdd5ee210d693c9c5b6c9b657acf2118cb2badac662b1c870ef2de48446ec564cd521050e452eed3b669f41c6657044a013ac034f961e312a64292a8f7553d25dde14a89b6fd7cfb01e635c75d8fc34ad8f6ff3fb02c8c122e79dd782b84294c52c7c0fdc9a929789cbda388c29751d5f62ae5482571527832d385fd46d894960f9dc6593c9c2ff836a5984800d003d8c62d9714c2dcf8c90bac157c3c95c9974306c6b52f8637e9189781f0b3bc1b16cb73af5d1719757eb188991da9339f170d000b82100d390870355b2aa5aa5aa3ac83f1abe9d387e52c85ab2cf256f1ed43d8d2d5ba3008d5807dc22351095696e95f53ff587e00d93c50b8f7a7c8065a7b4c41f391367d0f0964ac8b1b110d26f8551bf008481fa4dd1ef19e5c52fd15d3f998a2c34eec6bf50e5badc4b03ca8edc4987dbcd6b0caad041544deb970955e7900dc8650ba84d465ff9bd3f3d9c660f48eacba1e1fe5c2a8916e60b29052fad23a3a6305f876343add689cc4fa75645b81f31d45f59f14a8632ccf512a77ebfee2ae8e9380a6a4ea42913a441f9c6c6f42109199b97054a193eb041095c06dd9a17322dc7005c9a8be3eabbbf62a2ada398ea7538508febd5f4e115f3978b8d4a0bcb8a0c57ffd2b72c4bab375e9ef76e6b4007d7038ddcfab2377e4e09e4af8235e198c21a988d5cb3de6b78d3f718559213f11488ca025d5a608153595f8bddd03db9181d86f3e86293fa5ee580de702f9abd15529165a898eb5bc115c0c39cf9e722672b7f46eff0089556ffeff632af6e4c51bb641d57c2faaef7fb9c8e8e4082adfe0a30fc3e6f19837b57fce4658eb64a66ea38ac7253368459ae96e66470b7bafd678c8f37767d9e68aaa20e9e6a88b6aa0b9084dd1d5d79e28216038e9e38606e976d4ff08cd904a85e56f1c86d4b6f42ef2eb83670f0f23c9528cdc1b1700501798bcf7d68e338b551a59a992c8205abba44ab6a29795c27561f73072460e52b615b0ef1b4a43ef36f368d144be991ab99ca2b4c17e8dd43606c61b600afd3759a8d54a8c23869523bcda7f2512aef0c709958c82cdd7df57fa3ac7da67cd701738a94124347955634623e30bb9eb6f8cd9f0598a5709ebd91aeebd11c41f6bad67262d552fb2ea4b1844aaa714754cb965a10f92d4ce4d8b40de0bafae09dea2c4015f45a43624a6f28018500be5321cba36e8fb5e797193c15a2878af2513f75234aea1e4728a5c67c86d7236e22d53f0ed631f141939843b3395afc13523cdf5ad4d8241ecafeeb69fa4b73ec65b48dd171d3633ad0d644aff9d5c6dd626230fa8eff0f3aaa7e4fd84eaa5e4b5ebb80f4bb5e97026ec369e893d0583b9b46a4b248a2e027cd4215767d69ee84b8f5c2ea491fb302ccf618495cba5dcbe2afe62595bb9ee165119e072757cae545ce17749fc09d1a95a1db9e3e94c33f02516bade35799a3ba9c192c017689d1e01ff87562bd86f4f08cb8fabf0b01986488ce53e04bd0a67d09cb350dad646d2246386fbde4318c2b959ae1ec0d4287bdb7dbc9fa917088f48a68e0ea984748e01f28d4978f1bce8f0c383631b4d2ec34ad4f0124d685b92afb4a4acbad249225df684408ab745a6203ecf511b92685ae377271ebb546a3ef98425042b570ede07f3c074867a0e7b0157fec3301bfa5ad71f860db55c1cc442614661e5d1e574b337239527504d64d7007f3e41c2c5cab213824b6cb76fda9817db36d18908a2730467f2c74e18b5233a86b0cc85f1ca77903c16be5a77d1c3545700ee974fa75e51134f057cee2af736ba6ce7f33fe3d5ca33a97f0bbff508c7174aaa8bebf4c049bc5b0a4ed9b12a62eb4dfc3b11b4603a5870a894024f85acbcae8661c98333cc7154a8caabcb8c7cc8d853dbecb032a6062920fcce6efa691cbda2ce3bfb38b840a940f8186a5816367adeb62873b4a72020991b1020ff1863c4cae292cd9c8abffe6c4e05f2573ea8f0410bf0bfb55f5453c4cb6a1f463f439de4457eb5a88b72f78a42a84de87086153804bb1fd6cc9d756643aa9c37e7a275b8f58430323d920ad13e632bf8663dc103ea813735baf1390ddf18447ae0a98395fd241d6835ef304419ccfcf7c48836ce5b51b692f768a1e4b35a4497f76267c3c78d10d8e8656f512c029738e0ef33625fe43cc1ae3a485a688dbbd8305bc30da4aa524e976d5b8c2f0575e3441e84138c8c198ddb8431e3d0d8b1be3144e2605a2c6ed700f1d21b54ca6eb9d5f0a4243776c04930b02e82e9313bb22587ef084513f4643182b05b2c84a8219764d35ab0eb835844f390a991ca57f859cda174568a837b4cc2635be1da28cdddba413b61f672d7ca30cf5db0358ff6c2c68be3d26ed76d19eec2ddef71755eab6d017caec6c5cdd5f05f16ea28a992f4581272cbdbba85683237607d4d03b392fd41e38e19d533196fde106bed4e2b9b9ea20f4259fa06631ae25a4be8f31b51d974ace7223f717d0921bfbe193f853beebdacef4fd414a7a5afbf36b90bb7af5813e6476d7015a7ca4e7584d6dfa762e2884b8035cf1f1b24306aa7082606b37aaf19766995371d290ef6fc41d052021df30f5cd640c3d4dfef676f78e6a9409e5bc06ae66d2b784553dca23eb3a543d26b6f804a30192623f63a18340d9ac7c87d179c82feada6ff89888e4b7ecef822fc88f6a6ed188c7bdee6b649e50ffbe41deee84d1498891dd909101b71a2b8f7397c9df7589522805ba9e24386df1ff0120239c4d3285febeac9899f3c9f066890ba196dd3b220f0b9dab6d5c8e7c564f25a44f9115e597b9a8116711f34084cd6a1e47af09ec7c53e101292ffa54e3dfb4afa97ebde9b28c4c3f9bf3dcf03dc7cc26c0c1c1999cb066e9d6c222ceb645a19510ca072ba51081b6db35c37d679264de662bf334a7b89902e3383efa6c659379732cf0717f488006b10ae86e5e821f36661c4d24c1dc1c91f4b20f5efa533a78bfe4129d323726e112f02f36ee641016f22813d12eae91a19a70802beb39f6d3c5de99421d79e0ffb071a3305f9ed370c5dbc988b73d8179abb307988191ea98dedc0f8069fa463c03201819d8348be2e41508ae718ff6cca2c0ed70af41fa1226f24332cdda3f8d6d18e6ccbba8861f4d64dd22f753a20384781381a7e39aafaaa495bfdd7c267608400c49ec004a77ec91ac594f829d8e26b0dbc9fc1b9deba78cde78621e5fef0b8169a5c7eabe4f6eb8d6b07dc5607243b53855e84f7eff1595c6cc0a792410580adf2e2c539f612149878fb04bd8a4aa404d345b90130a0ca290e68e2e44149476b6bdea6b914fa77c887b32b0b32a2565c42129d1955dcc89fea917bb96afe8b344b424a562fed7d167bbe772e68b0d31cf73e50b1ca76d0fa775f22b3802c921867f7f38dcad14936b0ac13177092d2f7b19dd9d75ce950a62b3d16550a87b33de95ee8d42ebea2e694989d5d7e34f4d7836b020694af79201e67ff6607fb682a8ee817393b3cd85139f24e0c435a6dbc7395a489278fa5febca12928672bcbef3fb63e8792702e4bbdaeca9cbc444067a11d7aa10fac39f8e3f09f27c2d2b78b2828d81b328157842b3c3d3b0f95454ce6860ca02c56742bd71aca96009fa99ce877d5805a5b870ea88ca005a1890de5e8706a255fc8b4dfb0dc8bcb7438976e343889c21bddca77b2e18786e5953ccb2101d5d121eae17239bae1f9968cb430ec4fdeac4ef913076bc110a94221aae41bc53243e7d50a5dd1c3c9acaa5e35d9ff082e21be3e38b71942ab960ade1e305a06c234b698ab4fabf54ea97c3b7642246b7700138efb3c273336e8ad67770e5976c5d198280248d1f0e2ff5c653ee0b9a18afd32f117b96a0f137cb36a96697d3958076245cd3e8baedee3da63518cf92e37d4facb823bb6a9ca23efb549d09bd5843fa42bc9a4fd996c7d05f6ba7729520a95526477794db36db41c6718a0b5681886ddc91602f3b0ee9546b5ae17fb337d886f309f824fd2e36fda415e068c085bc50822680c867b5a8ab53529fc9caba7cf5ccb57465c43f8d3549f70f3d022c209d9542b27e2e181f3ae12acf49feffbd1ffc6e57d7ab2d8b80ccfe8019bf250cb2d6c6563d208fba5ee0c582657b3756bb1101452256b7c3ccdc7c667a990fda9daca40af18a3df2bf1444a602bcc22865e5a4fe81500dbe6c561942c86edcb5486d2932a9336771af4e1fcc3cf9c8864809e6e53c9b3ff31585f0c7e6dcc963d11787aaba1b6f290dfaf0dcaf8ea8a47c3acaaecfecf44fc23bef51a7d4794993721f4852b355ba99b34603487661fffdd567428d81c408a45e4d3f2464cb406f66995c7904fb0687c05c15644dcf89cffd65f546c309a62d36625ad9398e9ffd9d15f030e22d1c797216f957bfa7c9a13668014af8d51feb069b117adb4c9f7a72e4885d6e9cd632714718e0afd50d0f8b9b175e7a9da42632e2b4480c9247bfea922e301eebbc777f6554ae5ffb2ff13cc2c00a411b9193dd9cc040aaf38900b533ea523781c45cec451cceed217687120317721297f701955bb6759bc3362c4a01f9dd276f083500ef55dc913f9cac205f9555eb931bf12763aa563d7ee307d91dbf104872ce533a6c39a28457d1b35ff1b32d969f4d8b389abb71b79dd28e40f91add5d6e6b4bf0b2f15e3f46a021047d0f2282e87832609e129f55addd129dafac0bfe945cf931ef5ba1fdb909eab80f7a657eb59e82bfbf18a5f162d6b59b0cbccf19c1970556c5b07fee972d2f1ac9caf3ccf19c42bf27862d13686b761b5bcfbfbd4548f9c33f98732e3a418667324b353c722cc7c2b53f9ea156a97df80a56bec54d2e22b75c1cc3d612fce0548497f0626cdb8ca5789ebec55fb17f622d9faaa2e30ce1cbcfcf53a1942421564efe6058dad6b52e2a5451c0990ad391c449c696ed5d92fb55848ecf2d9e618e211384fbf5338c40c552df8191968cd8135a3502c092d1159ccf1953c7ee9fa4f2b5531c6fa4ce6164b69389d01f01b7b4620421a6b6bfcc3ddbbaf7ff21590a4ea7388a7874e4a378363ed565dece2ecf6db03c2a99d1cb85fcddc04bb7f92c9b2e954e63a4b6a1edea32a351022c4b1b30d520a309afa1fc451b7e60f5da6f3650912cb9255915bcecda789d881104199307777f513a9999664a7a17c45fb51c6c837125c1cb978bbbcc7f55146b36f6e5f5739390e632677290720a4ad1a0ec2a6ec2f704af491263c0daa47b7719069ce56fcd2ae5a2e5632d444b7ab18222f9e8cb830630e853426020c3d4b834b52e16ca1e9751913530a3caeb4c422f78837e43e709c00994c622a49abbec8cf581437b55c0134c66c3e9367d62053f9b238a2a932ddee78e56815decd0b5582f9deb5ecc62e8b30ec2c5e083d3e03204b7820874f5e112f8836f41b2affef1cfb145bcb352b2bface0d5a8d108467998a3ee1077135e929113d82ceb65409f6adeb223635ac508a7f4221a4d92aee09eef6545f6ec3793d0d4c6463e637f22e5587a3c8e9d8f56a43afaece21bde3e643723b01847227acd27e96b31fe556bc4b8737ddd852633bed1bfb1c9038320874cb39ee6906b4ab5a5300b51ff375c96c66c636c8351f6c38ec0dffaa25c56ff50de314995e43ce950e164a61227026ea5fcd543bca145d5a96c52112d94ee77d40671889af02f5b7b97f7477e72210ae9f6074fd41dad9584ba8aa55470ad76e4ace6b39367aa886f4655c188f6cb7a62189ac2289d22af6d97fa8935ea4543e4dee1ec101393e5e164f838b299c2757904e81ef0c0f71024270e77e97c32670263bda4342d723c5e60d2d71151a54393b712d64928864f571709f3270cb6bafe2416bf03e3457c092abd194b4f3ea60bfad28bb12775354b4517aaf7f3c102f0d7132da6b0b8dcaa23449950a366b15a55d18e65bad846ecf4d4475b0edc81a644189de3c82b98e3e173bc631bf7a61464afd06e60c40f04ccb1ca09164d75e77861ea28ef13b968145208226f3b8ee812b309605bbb3cc4baf27ac3050fe96381ec30e2d9a13ab880856a3de63a9dd12f4990da9f14844ae8c161cb1b72a9cee0bafd7fea43a02042b1ef906f7867e185d5d3afc18259ac2fcf5df4207ef6a2739fb1c7d1085d7bc8cf9bd1c11287382dfe1d797d8f608d0a9c541a3844b4f1518818e8b28ca9fe39fd67538f56a15952a0e9f366b47081c816a54d183cf70caa307c12e1a11bb65ca6ee9ad4e9862cd990b67f5dfa678cc359127b1948083960114fc6bfeb6a87974ed5143f7369219754df8b56800b42593f08505324b5071ee971a85c0998be98e7e6030f2a32912664efc094eb1ebe253932ccaadf112b7133a22fb7eac740959aea3157cf0308116088cec3d45882cea166b894d8823a585820c9d0d3ddd08b80bc0d549b4297bf683f547dcacb786f3df92f02c1ebc0009bc03e15bdc24034ad97b66434d4c033499d9a777b22166b6b55bddbdb6ea09a751b6b91250b3366cc608bbaa3e9a32f69761f80fde1539919d61b8a70fcf09abdcb00e5336ab8dc18ffeeec356c2286a8ebd49ba9e056e3c54d699b93163b36ec5bf0ecdc1afe0197ab3361ebe590dd6501f2d25f00d2834ea1276689d72195c0a3204589d32b3e7f90910dd03c4da281da840ca30fbb5581c6a347e5f0da3bcd93772c41a9ef5a5d45df1ec181b2b6cee6ae228bb319bf7e0edeb65cd70e48fe5402ab72fbc25b0725068f870f40d5623bfbe79cc9830eedcb3c32ff1393639944f16543cc19268c7e034c91a11d188115bca90e2b3d2a5837231da0f36f62e9e1a436fc4e45f40676a5b037945c975693eb4b070431f95940ed2e414ef1f8956dad35b8be6b56d503305e8bd7b844364d1c552b04fa3ce607f245741f0b47c5ff17d454bae4ff2e00ab75d6c985cf1a5453d9b74bf7ebe68c51fc7f07a3485d481e2a99099b01722c109769f9629c528f49df514f0bae699b0e4c73d56a46963e9a9e1233631b206766d629841c387b8f1b8c72c8938fe4293e7d87c86078bf2848e9b72fb0d473ebe26f3951414e47a9e476c4c60052365bdc2ae34b4b436baeafed3939d569ed6e1b75e740c0935b5e31d2d53d4d99441b5efe3035b75be5edb3b01c5063d5c72eed8d17a2fbbe274b40a333f60fcd00984de1029ee855402e562f87302ce8f19680bb3db74b4a69b65c6c4500f32c046d16aaeb622f614d6be3fabb19b71a889e79a2335ab66d8893226e69238c9100ed95494d2636f5a2a959c74aeb2bf6de6058174343006d22f1e86557f5db4d90a6e348bdf40efa36bb0ccf7cb128893488eede5e850eeff0dbf07cd738511f33c51c2403e1fb0b97d0ae3512e13ddcb2a818e7e71d5608314ee2771393c35f9458c274a5ae2fb7f990e50ed4e02fa14a7e62bc2333a7a7cefff8103956b7ea8a27dc6544fd45101447d14617c8fdbcd3ce21a697c2b087ec25f88223b50936d04ca5ca4a181f0c47439e48c4597e2f4afe7908b8de0e48d77abf708551eff9ecb35a73afedcbf04dc55f5b1bb88b66d365703425a1332beb64857e7633ae1ba03bddb8c330fc7e2135e863cfd9e27aa73ad16b7cb22d4e0747d6b773fd98e3f54514ca7d6d9d87ad5be7aa4289f36446f035e652ae1367fdd6abf115b641a54952aa47bcff25479093d317a9afe8248e023417708d380c6b1be6a721a369905525b4d51d29c254f4d152a7280156445504f1efd53a6040dbd886741b378c53a41dd0bb6e735a208256200891a0dc95241f910fdd4db713bcf30d154ad453881c1d66332cfe7f04abbd9065d5240f34cf5d2d7165c52ca804f3f7f8cab037d4c0ca64641ba4c5f123e104e4cff8004070d4f90be9e3637c05ab9b392ffa87fe90682de2026faa3a97a444ff5305e794eef22f0baa36afa1a48e9e49a45dd442678e2fe0adbecaba1e8085177f38d7cf3d3c8118d91d3ce378962f78c3edbe9ee32f7d3fcd3302794734e0e9381295f52f779d96c98796cefd2aa4e3d00f9f6350f0ff7d6fdb9bff9790ea12a813e73994194234058cb6408d54c161e9c4e0df669fcb8451b7425773915ff9b60de5c41cdb6677e7b1eebdd3338e980861b84fa561b26cf782be581fa76b37f1a6f091189024772111060e5a5c7cc12cba91c5e50624e8c7e7ef2ce6d36d19a172bcbf31b2b39266a0346bc8e157802ba99b9a05c839380f14806c61012bd65f4c936be7cd14564c31729543d409db15a38dfba5e518e2df61663edc36a4be42a1c940d10eda506a0078806604678bef10149f753a32e8df2e54ec1c024db4ab142a9f6212ce365f811f8faed15f8520a853946c7f1e2b008911e18d5c5fba77b334f9131f46e5589194f3b300a970b98459f7844ce0790eeeb255a8229577676c3493a91369cdae3bb96331d9f2a12a6b8dfe1fb0671cb5399afca5a7a8113abcf62c18110bae29593c1970b932608b849a9e7501af105f19d3eac2ca0a564ee37e820c5c8142c211bb0221cd5dffdf45d43c045c4b92d24489861beb4373d4305fb745f94c950a7bca8c4d53d2f3b81b3410a4ae0a9ab66c4a3d016571ce00e02678790c0878bfdf69b19c4135c82ca2127d17ca5e1cba064aa5d1c43c1a0ef96a77e14974dc5481ea5f7474162d1e8ad260d8ac6bf0dd1e551b5ad09e3aed830be50133e93f92a056900f64cd504dda13b2cc0f00fc76727a9360951977283fa5d3f0ef27f2a065ccdadceef7245f202dcc99d570dba0c33616d04e706699a4b9058d419431f91147530a613543e9aadfb1f99930c57655c0cbcf9b020b96539a612f0b9c17ccca0540da051aa536345268135577de96a4b6af66d1d11e50c351a37973c54dc6c458e2a0486296842e5efe4e9047dbd81239e4f7ffeea773ad1af4759476cb6a2f41bd23d279223a9660380fd62725379d87b67761438eaf813e9b6c73358d25df8da963e064f18bc296834ad6f50762669b7f6de120b1a1694f63a0c5b7c29a697d1131514a84deac512c246c5598ecb155064b5ca5b87a86b3ef1632b960b8e908235f8068f30b33bca7ae9b65f37853f8e06092e88bbee5a49e744c62fa5b8cde5ca71613020fcac8ecda6c4ecf081c738101cae6e46a8b74971b9448915b7825e6a61308ee8bc39bdcd9c0870902865f8de761d9ed53ac9825b5c0fba322006d3d0c465c2c1324bec9fe8f02bb470738cf956f943e0b3d860f3e4cc48846a43741ccf4cdc13f2803bf81f58b4764b3822f89be9538dc5b0c49e377242f4e17e3744381608d2e19165cc62db99e4874981b4c7d0cd090c5ef6cdda9ad975be24d7c10b69aa775e916aa1333d4eaa0745ae3addf15e6bc50609674eb6612664e87ea2009927d5fdd53c02a527b4628ee67eafc0792e9e5b2887c94c0fdb74d3166e5c760fc49dc5350954d8ad3769a9897f307cee4a1e9ccf38ab125f41eaa781ee58df4caba2fbb38934b4c5780471d42e5d2e5347a791afa48cb80aab86df2c15b1a4ef47f70c8822739c18ec7983c192a27e206645effd6fdad6dd0669cb66fb7c99cee6a368b599b3743c0f4f3e23b5c1bcdd0c8b468b5b0d4488fefce9e618b7f3bfbd7872606e5f6f5935c3eef320ba7dac52a02336a0a4383bfd1745d2bab9871df28411ba91d52111fcffd3c6cecc593d402c9d607b552176526b97bd2a092633bba9d0be20cf6e1a9718abb7edaab5d14aef4d5150bed6d53729fd1e581d9989ee95bc6b8e8fb92643bccd45ec78b8fb9269c22708a095043613aaad875ea5ec8f657542d81d19964d2795c53f9ee1e42078bfb60fab267368d9571176c4e421b57c96decb0bc58b1b951ba64cc1e97a666ef628ba0ffb3888806545159e557340f0e454833760f5d0e2e4b814f31f028f447fe58e57b0c165e9ace26a204584c307f796dba4c89d38db73974d30bad3fbc44946cc0bcfc893298dbb464ed91ed95e6a4905caec6c0d534e2960a2aa133f17bbf444f9112980fc53c7a835444f956dde959ca673a35d16b316b18ef0004f4229eead863f93edc87b73c15619511c3cb78c4ae20327d0ba0ec972a422ad30ccd73ef784a89861d21341609a5ff0380874aab73cc1a593eaec32ca57291137eb97a2f6f78a789bcb81e109015231f0aa31c4efb4a9b31916c241e322a8b7891b1f16281ac34fb5b56cb9f00d1620d8141c669e1205f7c94f658762ddf9049f3f29a19ec864b2e7b1b12d2aaa71885d317b3261fe964011a4ca474078dfe1cf881ff976b09411096aac37f887ffcc5d77755ceecd6b085b3e8b13285b7a9788ccf5a05081ff0a743e35843e24e8e2adcc5ce70dd78c8cc776fbb2e6e8dcc225f4e4be719ec3f287d1c2c54486394a49ac7198daec4a79d0457de8de785d98becc1937aadb2d5550920d1f4356005396b5af09b95fc1cecf851c8dc8e73e2372110431a4dd06c27864af31a61f76417dba7d7abe93ec378e664549807ffe61d14ac2b602f782567a5f3e7960a873ca93cfa24235b8cfa525c008296ffc91488ab2c57690823ecad34890fc1cb2b1eac588719db2ab2a7243541b1ec20830e19f97ac04e6d7ed655ca3f214bab15b207bf2bc4d0c16c1eb864270b30ddd80aaf858582dcfb0108ba2c535da69f4a79729c2f76e54d01156fce0e8f46a111896bb4feb1739099bcdbc6d3d67194f2a0e170a1f55e69e8eae4456e586b89fb972a729d2d0c8fb7847049697a0262e1fc5bcb0930182979dfd4c0b4502064c5322068b33bbfd1e872c12138df185ff2d3be00752848bdd9a0e700ee83c06179a8810d1b058786a83063c9a811897fd1e8a96152efe62fbe32435086698f2fdfa47522c167eaff49f8b158e0d1179371dd1f1bf13aa6ec8f5477b6d66e48d204c84d675622c1cf829b98ae1ab8fdb0f77d666561770965a14d212b25444b5e9fd481b9834a7d0cee6d58c4976e23894c189bc3948823c8dbc31169b37daed18725ae2eccad5329f59f2b64c96fbf70e5b4648fd623e9ff66d7335fbc221d2927f0e015016eaab9c2c117e03e59d0daf45435544e20737f64a4d57c52062ca01aff4c0493a63d607bc6e566f51e45a0e0cd3939bfe9c2903b245b0e6a2a9245aa0203e9c6f7b0b7d4908cbbf4077dbb50c614880406cd791e50debe841f857c1b719e9dd34e9b224abaa69061134495e4977a1c08e861864deacc7bd7678a8f92f51664ba4a405a3b047d4d33da75d6b8e88dc97d59cb956fc574bd08a2fdcff90f498b5e947cfa4b8852627cd1e2b9de8584dac14d7d2a5ac0aacaf5d3257ba7765c60f9b35bfb3c311a90c302c6df989bc74f789edf8232c90b6481df16f8150e78b9b3733b0a35d91de9b9632b43e3fb6fa3b6f2aa543d51fb6bfe42fb76a97de755ee60e97f2ebfb83100ecd614a9a7759863936360430f340f4e79cb35f3ee0d8dd3cf02b074362dfc3fb177dd42af149637268bdddc9be147ff5b25b07bff835f57e6f2bcea1e60de42f997b5e9bcb017a988f5e57d215a62c53b90f5eabe97116f14c4470c08007b83d2e75953010e2682bdaedc56ea8a29d3ab815d85291d0b0ba47068eaa21da623d21680483795ac0e820bd769fd1ec769d4707c2722620bb540b0c9610f0c18ac5ddff348085815ab43cc1fa7aa48e3c045e2e754ee08a716362bc2292e2084889e0290b44c5a8221b3370f802e54100716c99997a07628e07ae2cacc2484c20459fcd2a9a32c92f2f1c41b1bbd35d535a4478e762c68295a81559944824e9f6920febb6f3b70e8cf4e2a12826df686cbc5a6022db06df5af5d67233b37934360a3c2278429fe19d908d4b8fa1cae59164ffbe627db4fa6df032284879aeb3f66d42531e42adcb2ddd20562721595bb583da405cbc360348834340775dcad69b6ffe027b4c97a22bc3a611cc217686b3335bb6350ef560c01912b279941afa7f4998b0925cbf32d5928a425974029760ec424a06bb7ade6c3cdbf589d78d9acff25c9528ed4d6a66079987d62becc340d52651753c9472570d9789b9034741acc346cceb01d806762e3e44a61230263f9cbfca8086296d1209028d2c229b70c21c2b46290f608af0e9b14747be439f4cf87d3bdf2d9abba814458a8f83b090f947800e64442e1cc021df665ed7c97694924831747da90966e3ac4e16157df295c6ed3a4a622c7d752c1ce14040c793b5544c5cfb9f98c89dec6cac6977752310d94ae09e41053e88d69768cf06b9e31cfdbc308f7fad0e2574fc9f3d112e017094c6ab20a0a4fe2e0a2c98c8ba7386f13b169bc63fb4b5e72a6eba7609b6870b758bbe7d3b97a9cd26a9d1d676267a6df986f4bee3315f90e2b56f678100d0b283fd457b63be094d527149f73a5d2b4a51994c075a21857c6a01bff7f2e89c3d86aff785dc66671407c3e771c221b0417ff717403b27baa17c2e4c3de94663a0a3e5a888b9664513c03770fd4ccb1e002d66d1efa4c27a7506c6e5d7e5a77fd16f0a33bab21cbac67ca054c4c9c22423467dabdc94336052190063c2d97fc15ef7a3d9443836afc19e99535963ca89b708d490a8f522edcc6762103c39a42227da74b58bda997fe58e87327530c235fb512234831db7e824beff6eaba466a512ef41feca401a84cdf989d3cc03dc768592839a0712d13593b811c89894f5a55dc7212f46e7e89a3a492d45e6308ece3a3dc45550e68f8229eba9e6e1553ea215c819ae431780ddd6214c8c3138362d22fed3e5392ac0ff6d220b3e79ce31f52406a6a01ac87e6797df40648601c41c580975b7f9e6fc80f4e15a54eae1d9b0da25260b0056a5a82efde2cbdb52a1f0751883c08840e57509d00a8ede5617a132e757918ab10ba48855203546ac0d9f0ebb8915979ea3ef1d118d5abac81ef112ecec8d940febedd182623513978fba5597a320f3778e057ff633d943bac372885067d1739fe213d0172d3059c336184ce59e186233736b4366699b431a4d5e9943cfb579430f077c4506fb4ea72954632d252350ae0d90428ea1a1cee2b0f755f51938ae7ee458c1e74fee0d4979cee9b5db51f9efa53f2e509e782c191974b045ce800a62df0fbf2c0af30c27dc6e60bd2c18c71682989eced239e633c61de204b1ba7f490b0bf5dd524d99c408e525edca6e45da05814fe6229ac868552220a8845f53332e08103a269de78111f6958560e8c3d308b9d218d63c98cb4321ba9d38b563ff2ff34c1555fdbff722ca3beae44ad6002f55241dafdeb99269d9bd91e489ff5303d6457cf88774a1436efbbe5bdda9f717a915553054e4ce957869f471122e9d08e042c842ce87019ec2987705119c2ba849e936a5dbacafd73e9c16cf7a7ed22bea9eb95e1203e0067775efb4c5ff0de0e7a6a5e4acda65864a784b884772df964294107329062e35261ef8cd04128d87378d187d9c00d8f56843ed4aad476dee8fe20084380e3243d313bc22a7f9accad76b2ff3bb4def81ad2aa159607aa8c12a5ae4ae105b1e5498335762d6001cd644a61a71dbeec5283cf8d976e68a9c3a7f6422b99ea442f79cbf0cd94206e76d7bf8c36039396db05aa8c67c5ea7b294ce00e8407dc25d093797878cfc4f8f406d46f007251d9f0eae7f96b58725503d38b047665c455a06fa649c6c7b85051beeec9660eb36c5dcad7309bd2c566547f9b16344c756db57f3a91879f174111e4c92369abd23b01a2e148eade299da804176a6f42e49f8abcbc3d40529f43c53763db863ed8886b40f656ded3fbff3fd53e0f7eb8ebfe9da5fc2c5b933698414542d180131250622428af6c490eb1602626d632e7f6490b4ae799ce59cff6a1ba3b253488acf06e30dbfb12fa805f604a47f300dafb87b486a3e9a0a5f2fab6f92d9ac85d5e4881cae76cdc500c715e223de97865b6c782714b048e147dedd9b2b097810db3e6b574a84c9e9e2e4811e3771c418985c152c5ff31587edb070bf259d50a9d49a638858146cb24184bffc0eb1f820497fd98ddd8f67f6b959bf8389fdfc324066617f140c9d8b4d293b09cfb1e945281d74804b2d5f60b0b26dfd00a595cbe5fd9f5f2564da8a39c056f1039c2de0cf1c13164894b7705899c46dd60cfd22630dfca4c42435cef6d23505f18d32b509aa78157d2d9cd75cb364f75ff9bcff1c6cab27223d8ed198af9ec784627f5255c4850861838e5616c027b8bff8a5146d732d17b786d5c39f13531e615a5628d6e6012fee16412430ac9c672bf52e8bde03c48bdb188425fa8bbe706031cfa8a66da2992ed08c2a54eb1a2d127a5af3f296fa13eed35bf30d43ddace9e8d33f2da78eeb056c686d92757bb360c5bacaa809e91a9c29d73f5d94fcf97734f39a34d270ee2def8049ce2c5c760d4b0ec3e988da4bc74af5eff8db8efe511077395b5cd4b35a85d046bbe7631336582b54df5f008748ff115966108285a0f4b35a420a24b571ba0085a3f4708d979198a9b59a3dbfe7a4a0e350f59fa9d9f250cafa1fc94d0b74b73f6797fdc48ca9e70874cc7647b39096bd6f959605f41602b746dee98d95faf29cfd1a0b80982fba725fb55ec473e0f845eefc3be7b18b1ab581e8bd5ead1220266f3611f9723230f455cc1adb91c6edf3465253f3b4de7ecf0c16830d2bf7fd8b9d12304cb2612cc6720174ff67b2bb84d84d7887630f7fdcaf200a501fd4cb75529ded10fb6ef1c1bc89c8fe4d887747e4948ae3ec09adf99905fec3e5a98921c9284958241fea73dd15e6bec01336df2bed0c13cc2a3285be4d30d5929bc76dd2b9de8fb9d5da3b73a4fc90c06617c5b20e2dbb9524ea4426b30963f6d025e458ee30b79e678f8569e02820f223639d6874e4b7d46d5b1f1589f0e02b3043d38f83bbb42c2a7b1e348d15259f56dcfad279d82cec18bf73cb7f9ae9d442dd523472159adae0b71d0043230438f0976ea5a7f51fdde005a435c38e69b60c06cc87b6b902187c5f148003f781a68cdfa339cf056f37810200eb8a36aa19b2388cb6725262196e27a632081604a386ad33fe369847a15f589ec6aefb904507fbd7b9c18a61b63561998a95c723b3ad381e74b5deb9f2251f80cfca4401ff419980d790432a18cfe2026cd6a3045eada2824130d44205233832c13a658875e1f4de507bbed43f3040196f3774cb7efdc7e81b6733e8550369678707b45cee9de008b876988f31c9b435fcea0bf363b8f89c4f5dc0d2bdc635db9e12a58dbebbb6f39a69931a0ed6a2e9f33fbb1540f7eb26e0b6327f80fd74f79984d506aad4919e1200257a0a341938e451ec4ff5a7f0c0d4c11f2be8b1506f563bab976d8150636000176bff6c5793950f6e7450aca091931bf5904bd5dcaa037dcf6d63ef6a67f3a87c1d7d17da511f57362885e280cc1b3e242406991499231be62db3cfa6106c073a8148165f468d57ffb41803e700e92038dddc466f93c014bf6052c3d3d77889085313b32f3181b0df87b4e1436625a839e0578be8a88a4743a672a9dbafd3ff2a8a78526552fd63aca669f1305c3780efcc3626574a47d7b1517d5ca0c614e42230782421c99ec3e051e3655a3fc0d8c52db6e832d9fdadc8f316df1bb49245306e504c6a0d029107bb63656e2c2085011bfaab460b4b08956f48bddeb6f1e6b981d34757f61c465207a95fba9717dd2b2b65886cb368c3082708882298cf5b1b5beba8608a495cd81bf8f12cfe2dec576934144a470f6f8038abc82a3d7da407e0cf3e5e84dd97220ad85884139db8ff1d1a49f929b198e0aae86a087950e83f1b858538708085916f5b733b49a07960d401c44b4ef52c5b904a646b4d022e7d687e76dfe61b9350a8e2b69b974fddeb420ee71085effa594e472b7013296ebef7d5f4a541ac739a53ab4a67b15a22eb7daaf773cfcb9c978e0b1df9e58a08a0bb1c145fca72998187c1acdd33d0123fa51c1ef3f8946986592a669c5eac5566fe33f9cace449c89545bbf3e8b816f9a098a991bec2a6f52db75728434093e309d3787de7faef859efd39bb9829aa319e1ea8de0c5d5646b7490f5683421fafc599166d0fc622c8166ff3cb19457d5a69d3e878ce3939e1e032305c7ed8a229ffdb674ecdff72c7085758b252aecabe2f86557b0f36e6ac4503e704f0d59ccbca37b4bec24a7bfc22173682267c387b42d63c774901fbf4530f0aad9204d9700ce9704114bf5fcad54c3301440f6fef125b8a01eb906da2bbf2069dfcef40c4d4ee1e04b49a47237b212cc6524a6e1cdf1dfb0aa7f93e029b0654ba9502ac4b8f077240b5f04daf6b3b8d008416a6cb14353ee6bd783a9ab818bdb8c2b9a622ab6539d47f28f66e151a4817fb1cac287cc0cf9e617048c99d87877d2e68c119ffbe06db9f0b750620936f0827ca369456a156cea60855c7d1dc735e6d13c8957510cc28f0aadf26e13f96c813f0e76465899cb67c17f26aeb41369b36c062b5ec890dfe1fc97ec4cd9506c6f259e2cccb03828c13e04defb6d4558d10299d48a3709f26ee88617d60d5f9ac9d0b73eaaf497ceaa312c5d76771a6a2e4d42548e2333b9a50e1f6dc487024cbed63b47453f9869de25a55ea24aaec6a46c3e2a1245001441dcc4741943071d7a3020852db4fc0017b1dee391719cb2cf9db4de0f98a4f78894463ac2920879767630efdef035338677eae2b7697f77b1867b00960c1b9de669af7914c3e1ab5c86fd62d82e742c11a99a1afb5cc4736d171b2a10c4c33ae325a4db9d27be42da584179213f0ff34d413985b8374a7b255a7ed9372d66613a9ac418d49f989d457abe622c23d927a9e168b2c5434819bed4dbbac576a13767291e947703df8edaf8841394f98ec3e3f0a87a93499763cfed319674dab1933af8fa1629c4839b3de196dc8d9a88aaa5ff5aac60100de468f031aa0a8195d5aba4c00465e22ba210aa1b01a63aa3e9f756de9452ce35ada10980cd6231441fa107c5d04b0cb879c51a702881d25791e7f46391a707599978606161b524b89fa24e294ec629a2fabfbaec9f07c9ca9072d9f2a5d8c95c293afa79901cae4e45c4637822dc115e5d19a0a383ab123eaa4418700a02f7459636b857d87a1de7595f5b2792e29ff983f97218262d6f802a74072130a4234aca8e8c5009175d89f2c9e90937d4f4e2c4cc01eebf8fdf1f871ec4c36cb6189127d36ad8ba40f6e6b0d60dfc23e2cd37fbbf55ee2d3f1f8d104e90d467909003a693e335bbc246818d88ddce05016f23670b42b6d577fd133d9a6b3f1691f3d17ad90ecee0d9fc8350da6701961e383248d551559416dcbe6b2d062c7a6a13d804f388407c29a53c9482224b9e8566f41157952f60f2c9adee7e25b548685049ff66b75c988d4b662e00309c582ea15f3d5c5425596e06648517e18dc0574680ef29728a50d3bafd1fa0aff8f70a247a5687ce8774638be7454b3d0018a2762b50cd6744dd8956a3b43959573c77d3cf4a7068322ef881a17a8f95fc08defcd1f3157c64910d4f8c9ff0d5f800fc2e56a0d4862a8dc2cb4b9695fc9945441fc318f8deeb97827efa3f45c731ecd0cc9f47bfb90079138e71fffff1e5fbba028b324c4bbfd5ee07496b28124981b6fa6c48009c8772bfbec4bfa188a1fe155e4a9a7abba885d26d8fedd517bdc415939fe05fb1000eb03b05a9cf150f058cb19082b8dae602c9db736ca55c3c4781bedbc64c662f5ec64c76305beb9c46ab699fdaa70ea46044b3c2abb7e4579b6ac224dadd99d4385061ae63b5ea1b262661b277bf53eb14badbe2fc6847a1ab7f1cb85e84d8efb66ca238a5aed7a95a603abaade55b3c52ddabb63539a3490e106913df829c447aacaad8d8b9dabcf8798d9923447a069ff6ddd7bbcd97e20b67e4229def1705d963f2ad128e2e2e6aeee11426687676b7180b7673a962b3e4bed8ca78e03ce33e711dc186a0bfd4ab54dec614dd534770200a115ac3fda27786fa611760b1e733d7ef3cbe96eb98046f90fe6558148c8b80e9c05908524046b2be029a3705cec5465d82fccab3fdcdf179438c84daa1a6c6b2049f0dc7525adf9ab9d3ef171eb3b1582da07311b158177822cf3e140ad2350b1d5a935f7d09c57f228faf6d19af0f54cf2cc065defb215315205acc7b3a9e3b47fe7ab75faa6d065be18cf389eb603ab841877023bcafd678255b525cff23c84e1a2511350df10f5a8601e32c17e387231577568e3e01be15b1b35961b8ffd8bc1829b2645280059f3e53d4e2e57c3917a3588a202da313c6b579e2cd14ca4395a166eac98cc63ebec895a9576a36c5558bc5674b94c258f265b701d957d82de42595dc9df4d5e405c5bd10cd0f23dac2740f35cef51fd1a7296f461023464eacbfd4b98b41fed3cb1a0c67691d0a0b643845aee5c1a751158694b136f0c2060e82ffc9bb8a5f43fd416198409be5045e640277bc0b420203a54f51752b50b2cf5c5ee3e237783441a924e92341b1bdf37fdf4ff55f0cf5b55e3a69cc54b07ca05fb725b2534c6cd7dd077752cb9cb0b5d6fc83167a7c9075e53e5260147800f7735b45ad06edef974821ace8da26573c7bf6e2ba7910782f214b36b029315e635134155285c7e28bf72954fc5baf632c892a426624e6810a1e089d120bfa61209455396be2fdb74671a383be994ff2e2384ccfbc6e59e2505313facaaf0f49ff7afe6f3f722bc2b90076f624c83099d05dd0f36fd5b83064133291c94e20c3e350148701f92277d4ad2805686e4dca672ebf4de7304c0ba51689c0c11966a7c7b18d8fa228a94baa7b1347ab57a82c77cfce0b24ccf01f774c4ce3f0b62e9fc592677f5f59fb22df62bf72df3cb2859b1577709f13be3e17cf98f973ccbe5a9f4c8d207f2fe067494c9b6c516505c42b5e4acdb660140df9c0bd9076e604a3d5051e5307e2401f9e7d660a9f63c6461a5746f1af24fc54d59bc74c257da86403d4b9b16469146d2714f2dacd354db8454a1544eb33ca5aa7f57756ed4794124839cbacceb28064d0557723824c16a00f3f07547f5f068334c687dc38a895aafb0450e187fe16bc7ca0ebcaaa8dd28073a6b142d4f105c82923df2a8caecb4b9700f654a29d2eb894b449976bfe2a6a90383c83b2b2c3b7070a4d9148c2cc538fc1a3bc458ea4cb58be82f65efb85963668fd478dc62c9e4dd188dd4ef6f240683fecf44c32944c2798bd94bc6f6d7074e7b9c1a9bb7038418d40b107744e6933bc63909b15ee7c0fd87cff5e6728d08646bf0c86fd07f7091a09ddb2596a36468edc106ced0a5cda11841e5986aac22ff7981ee86b8d2c2c3a2bae42385b8694b2b1b1382c5fc8054955194cd3eff2ec64647944fbd80cd5b286a702b8d92bbcc10f9b0aa8a734094e6a8f97eb5c95195ae98981f6902e517e95b8cd12f50cea034741ae22def3de817de938594bee1ee263169c94256b5a66fbc141ed914ec5cdffaa12e3cdfec1b1725bf7e280248196290546418fb54f4f0c5ec441039f97d8b5c1601b1793e18d6cba060b107b288b2023acf61c127706a19e1cc6e0faeff57a816900ed730700a24db3ef43d892ff9578192d3856bc9cba6f4441fa3caef3ec22fa69c2791ef40f7cb0e93daefec3836acb718d1ef23ecbfef6757ed2fa06dfe72c7908fb6458b79a5cd03991a443b1cdb644209589444a04df380da4d682542215a76c22ac30de6fe4d38f9b6e192690413564823e11841a3669ea1e2754770b047f4c28ad4e841bdae35914a7e36c4a1710b6a5e6e4c65cfc049b23252cbbc323ab31d6fff4bca1246767dd4a21e591bd8b515cbeda267b52add6c61b7f48f5a5748ab37f4738b51a1f2e137ae093283c9f761505bb75377840ba40a4ae6a127047550442c9c96760c5e46fbf093623da75eb653cccab96423fd3cbebe48202e6f552d59a2db49ba50a4da715646046faa8dfba8d810fa72f9d083ee2a0787420e4473e62b375608cf1df5704baeb7ca235331a20a53a743bdf5af1afe5d89dfe3767dffd441bd997ee4982dc519754fcef471754271e1d1f8d54d04b68bc7d21b9e2cbf5122077daa02a02707d37e615e5204938f156fe32b9d9bec25e4db5c5df602c4b57147a816114b5feee111dc92bd1ead68d1fe0fd79c28455d464f532c3f1b25ff1db0a22e5174022c17cc7ac039391309e2418bbc9992cffc6155468307a3cd1221fcaba6026ff8d16f64f5588bf0034997ebbaeec0dad7463badf46c7bac81d222450748d615a939603859d6309d58fd3af06796c9c91a2f5c40ec9d18f4aca60f08e2cec3595dbd15f538ab68b0177b4e451262face652939ec6ed01ee2820b4fc068dd6ccc42fde627c61638e82d2f410667c937d47f6158cbaff2d6b61234d86ecf5d3a390cd4bc4d41bed7e6849aaf2990ad3fca10b3d874a114a373f59d56f780108781e15fc21a6555a44a1e390ec508aba494990459bc374fbe80767904cc244dd25f72a6d0cdf995cd96317ca2fd428c0047fa82e0896b1b4dffa68b8259c12c454b4e7ca3981686f330f31a014779c0b83a71976c03848815ad529f5bb8774f8271c2157978c28047c0e55c5594c9a2017ae34a0cc35baa25da96992681b0ea1307a5d4fb36fc126dfb85a1850b5b66c2ca679d4b7d1985d0510c537f4fe994ecae6cbc5dc8d850011b59c746cd7e9b87363a411f9dc48ab14b66673dbcf0647522554d74e6f2495a9c7e3f908a04844092c6187e059c69a814b3d084a4dfb8c16db2a1dae8ffc23cd86c349599801eeeae556f0da1110db60a500d6c0eb8314757f7da136d1c753c236de0db793fd4a9e4207bb20d6423ded9b9f020ca5f17166b71003a048f55677e05f7351e07dba0aa0f08307a192553c514dbef6e05418ee341ee35655736a4ea3552f75b4cecba7cbebad0187577f10d6f44e96dd69d8420aef3b2e3ea5708547a9b27677a983c134af8bb7a420d0b0d243784cdfbdb5f3bd8b1c01ce91f46cfc75736e962e4b7f021666847a8901f55414db371c95582834c1ecdd0e3ddd92a9a74736a1bf1eba8f1b9ffe0c3ee266e5b2d1220b15650214e523c6a682d882c613e9eeb325ed5c8468a134db7f6ceca28a694dc3bbabeaefa60e732a601d4c8b74b752f591111b98f5628422f0d45a137eee94199f34119b860c8f9d05a8c7e76e482072875ea03ee853e8968d0e0b942e84ac25bfc5a3377feb3c5b80959b0ebe5ad5d731c056fc6033eea6b63b586c304dd3137b7d64191acf7c7ac884b47765ebdbad0031dfc6f75092ef755362d4d96e38879c241ae1a1e948041750d080f17912e5f35c961aab14abe58f02c797c88ba753612ef1fd4092ed2ac816694d0401eb2a5663cb49ec93445183a9e5a9922f75c1bf805de4897cdd2a147c0874df8b185181bc6a29c84948f68e4e78c669bd80eb3dc1c64602ddcc6876a7747c38fc9835bf29bd51720bdf15381aceb5b3a18055f389b5e9e31c293f9a52ed4a5dd8a29b14198b5c846246af9729b7c9102b36fab08f1165933127acb01fdc7f2af4a03e22929017ad3d3d5da2b67c188ce633e9bb8c5645b82152b34f8ebe1a4e85b3cca8f08e9e643408d3704837b9368f82e12a6573919141078466d39a2e8373a4a9d5fd323b7c000f31ce823ff342536d171894e70ed467b6f17b2611a71c9d756c74dcfc8100ea1b2c9371faa47d90bc146b3d1e701b03d497fdfeb13909e28c89d5d4f21e5abb775f7779c69924e6149885f33e4ff4fdc8a8fd7938e5c11fc1b0687893aebac465d37ba2d4048325004e9a7f85539ccb7f5b06b43f4ffda691e3c017348fed69b71a82da42617fc72649beed517a133e937f8bf628c5e49168aa3ea6c981b993b569e1d2f82e013b59bcec63336ef360d7c39ab4473a03d41eff7cefd4572d055ede15c540f55f797c7e246887627d4f56b7815e746c40b43aee687479ec41382022347a25264c020ddff33521e2861aeb44fb079741b012db902b88f2ba8c18431579f66e3d22058da828b395be040e6068f31870e7ba20b707b106dca2261485372fa7d1aea9926ecc2ad4ad1909f23a32c488e8cafc9bf2865ecb5b1e704852cf94ba4508fb60f954d85c1580288dcece262b2f1cdc32cd15134ad2ab21cf81b36003b14147863faa80bbc5105a67fb30bd2c31840b7f43ca6204609fad8e7a39d96c463cd2007902152d0a0579f81f139661aca63775c235ca979fa66742611582fb159201b5aa25bb897042a65cb038521ed7be132d101ae0fb87b91713a3bc9bf545668e4f4a8c093a880aad7f3fe01e31a073a266706f58ccecab97d9cdb246f79d697ab693fecbb87314980cb53fc09a42348776a25255df23666c6a146e2590f16686b04457741e78448b70fbe4bc9cce16103d51a33674773a619019e3a270ed8581fa00df04f5390ad0ea2f3581beb330bbe881f57f725d15362714258358c53d0db68a7f8d7b7ae92dd5d4f64f35a0cb7dd3647de72af580a57d7780df79f9f94474f3e0f8a3081897c3516a222b6c86936a334fe3d9effa3dc5a1aec03318fdd6937f30ec63df7c8e21e6af460c8a26ba13a935cb444ceff5fd1e114b2d4aa1d35e7bd3b7ce7d6a156f0420b608596254e6fb16d01ffb42c26e67b0824c4b3b3a1960a806dc15a56a85d14006202995f305d70176d20dc7ad4b432d306fac29f4be89a2d1ebf1663a519927f7b361da7fd9256c2052284665be2df2e807a3884aa774dd0f7eed96170542e7e755a3572718a943c3d94f6d3a7018bf738dcc004d630c960e6e1370c1044ec52d09957b4bb5a8eb46f93fdb2a9141fd66f77b5daba68934fc33d145bbe697809f2ee56d2b04d2e8d6f605ca488442d480cd82717b0d5a510a794b8b9c93f372af3e76c90b502d93f9d33b6b98256fe86ff122e8d24278741242ec04b3566610dfed50eb38f204863c1a0929268531a712ea61ccfd02431d26bcae3420b06479ad042684d1ed011e7a2633746a020c5ad535143536c096220052a27ba09da70ae4b9c4ce4424d20dfd514559081057d0e7f6ee136aeda09dbe0bf4be871b4d7101c0476aa25516a907c3e61a0eb921dd7df6b6efa712e8771cd06ad7dd470bf4f7f5d7310b7cb2d2e64f92d52c2c6c9641f1c5b1a1e27266898d55bf834506728a209b63649bfefda230382538196b9a0c322b3d32a9576f345ee3cec5cb92b64dff0b1919dcd96d023f8295fe2be91c0fd243da9889ef4f3350e525821325a63fe1bd1871d58679781934eb2e5e124b131c03970a0882c5d37ca584caa82777daabbe029e589901dd68cdc23bfbf345383677b0aa1de5fc357e4ad44a0f6593c6310e355f9d33cece877794d0a08b3934d0a2855178693230a1c34b6186f12d617ec043736ff27ec6d87382157d539fcde6b2b5f3416da30d59b6c2ed3578125f9e82b5c0f7752dbf70d6577ded65295614e4fb07cbcbbbd1f36f038d7a7e6ccd23b4cd1e1dc7136b69e2db0f2e682fc1359d8667c5e44089c2c68b8b23ef6f71df9075d2c7fdb5b61c26083703f8ce7eaa8327cd8e51251e9672077aca7e699ed8e72e5a3b70c56db116e0ce3322a8ce92272955d8e68bbbe913cd5b178ab1ee89999d3929f8284258ec8cf729e35759571be0c120866f94b81b38779eef502ace903c88b483c66fa9d9589e739d265f5e76eaef177dd77ab3dd4e9d249170abd15b2fbf558e18e53bf54a14abf144a50dd76699dc55559fc140464c379e92fd034eed9d31fa151213afdb5f7388cd8f23d905ad1796be56ce13c1815c5828c48c2aa77692cfb050c0145c4d2af780c4f044d200db41d2eb9fe50c5c175d89c9ba9de42bcedc8b23687101e94c8d6eab17f6b3d15e5edacee1e0804c2b950b2b94c63f79d070f736c29fa2ea2c01235bd55ed7c91d930130344e7b1748a5f76acffe6f985a03cd412e94a6dfe06554bb92a1df1fe87d06fb01f96f4d6b3133f5fd026eb500ab24697cdf1884a027ab2b0dc49d71764fad0de4eb1b6ecc1168d7b8ee7c8efe5a1e6d0e2e20d05fb5ae002b91027e19ab4c0b0f8b9574c39052192d917bdfc672bd86f4abbe641093abd898ed24a783bc0f9a3dfe7c2a82a0a2019f31faae2cfb323dc3a1c625d533a152c8e33f23df7730ca383edb822b5263f032a8b370404a572b3bd8f9f15d263165d62fb5930bfa996570aafff5b4e4ca0453c9298dabce7f3af2c9a12777a0a08d6f85a63408a79f2808fc34ecc4fc14e466140cd841051a151b2471631f8bc454c4e506eb3c36b23a1fb39fc1d9f83ccf968c23b82dc07488c3dfc5d62d99823f8599ed1a7cba3e1ea7ba819728bd886ae226dbec9d4bf04a3206eeeaa6c589cf44aaebdd758a068be9d3c5f6685b92eab9a10218a8a1b128a6025357d24445e30b7baddff53c1470b8c4ffe479bb525e48bb55b6768e7cf676618928f73c6c139b03ccc3525487b802744559c7725e3a25d266e72292798f45cfe849a618d8e39bca63bfd8935cf3db9e065a8cf8c3f858f330d94b30527487252fcc44ed4b0959087ff816eb1cad130790e5edc7b54d8135f3d49f32d681dea40de6427d451feabb5cb0b40d76708ce9e8cc38ff98091b46393ba4c02f0ea40b405b0ba7c3633c31f4c6dcad9046732691bab0c565158ab5b9a62c06eec490878d568b637aee7e5bd43b61a48fdd553b25e359a08eb2d4ff22375192c71862e539214e5f2a5c607c217b6c5057f4f0a330d6127d7ffe22ee6e8a2da141c927a1ef63c70b3d4bf0b3ed966bcdb4363575813ff5db98f0a3fa7d9239e4c4ce4c94f3e3d0d9444fb8374ed58e170030425ced4a8be418ea19117e6ff5060fda1c713930e0fd402857c1a6dc149e3ae4ef2105f4210d287646057a8341ccb3c1dcc2cdad3e93529d345e34fdfc0c78a931ad069464e13bad2c0c11f686d9d3253f2d4b1c5563a774854583272f2df40cdee1e38b51a43f401c7b57afcde1f1657b5d43068bea9db177c946158895f66fc2e9e270484f5943b13a09be9c3ede916c29e1a01404323e70851b0c7dbaad2b4382197f2eec64a04fa09118ec8c97448e02078bca7a746047cfc58ecbf39a9103360ae9b2e3423290c2a03be8d81e94c6c48041654ed2f753a5c08eaa7effee183dc60e66f75e491eb38dc2bbd016fbea02499310140eecf9eb51a02483207486ed425e34b9dffd27ca104784ce503eb672a200f7ea778c1e2ca448622b6f8ee6c4eb6fd74a32cef22bfb58199c41d7d140d48e43c5a8875ade2a7350ef0e0badd63216bfe83ec93134e6e914fda079450c571dd7b22d8025b8084a41527a2b818e5e78e97cfb61faa8fc7f8a441fd2a084e6012bb4ca21ca9e8c19b1ef430dd4ea9a7d6b3d67153d1815f1713452954c0da979d7e6ea2b8e9cd6d4d11823fbad9916afee672c2a718c0201cf319aaa31fc141fcdec0621036cddd7fc9f9c66265160b4555d80980746b7aa4192a5f343240383a90e09d02804e82b932c6de354934553d438707670751ee709b31d5dbe7e5b49f56b6dd5b5725741f025f20b791868a475e5146fa50a7e66513a6ffd59252ab91283113f67fcbf560bdb65aab72ac1714382f77fda52224ff6277938d9c0779833913b092072576a00ae85ba3c9c58faaa83643f25065c0db1fee3bf878c5d2cc640e91845af1e89b1bd800ade4fc6a9c643ee55c297d938bea0059baf2b700fbf87f0dd13d44fd27ac57b31e7f65695c27b50843d52f727f55b39f89ba8ddc9b5d6f0f50f1c175004a38ff6062394b1bee4e187f8ca2910bc752fb635960c857b091751d34eeceb3bf2a89792da2f02a754ce52a6e6df44980296a107205be006cfaa5f2cbd2c4ada481e0c729cd908bd07c225670a6b65eb3cdad323f6eca92f197327940ece062b7644a2a7b7de407f06fd51ef0d69d2c1da4f55ad020e1d30ef5f4c1a0615eee078df648554cc2ff48a5b8c5643f7a1a1c1a333d16bc24284942d538b44b982922e82e5b494aab6fa3212ce369be8aa2c6e8ed33d88f6b566dc493039dd6142bdce6b72b89e43ba3169e4f2fde91e527672c6d7849cf97c0f9179a5902691088502ef740f5761cd8a9aa8fc227f2212b1084486cb8bb470980a8e8d96abe11ec10b0c4b0b8cf8777cbfe506181a413fee40bd91d14470588218939d537ecaf429b978907c52245edb2ff449e06c109f7333c3810da5e96b302df54de011aa80e9abdb8b4a646e009efa9b122e9f0e4a3d1acc981f0444481996ecd5f53836643df9dfc70fafe822b5d6dc3d6edae8931b4a676368f28bfd621940bf9114411d8e00ec57525ee7039328c08b0bbb6f47f2be0494e12e8c24bf1debd3a3957f98be1923e7592c1e0e8e07abd39cbf556243e6a2d79c21023c63262e87e75af6d44e4c16f315ab568b902d0a9dd0cab2557d7efab59543df4f86602b2b17a4638dd85358a3757641e0e8815cecf1285e3ad4121d824f7eca18e58de27803158362363d7839b6f27d971db2c65b57c8fa6a67e2df9d5ee98d0c4cf892be461d34e2dd7541e702d7a553fb96380da44c2e7d27a4c54a75b7b0f37652788e2f563a742f40aa5caac83234ab35d229fa7089b786cf7518fb1a6b173f040d1889b472721a25eb3f538619dcf94bd8a6abb9fa0832190dadcb791392d438e022b808a409cee619dda17d0d5ddf619a63065abd8015a424822c724bf2ed9d0b9a143dab826d36147f7b06eb734508559ffa3a18f59f568b77adfb4ee773e816f66ff678f7fc4c94dad14302074cd046bbcd5c0f329ec055eaaa71bd56a19f57063bf471a9fb913c8363cd52c65803aed71dc7b9524a245a153b729ee8e727b635e50e679f1e34e14e812e2bcd05ea2bb66aa020fc26d247486ef77c44248eecba359f8981218edf483d41e2bce0c51d3fccc467cc4af175e5af4a4cd11535ca6f47fb75d0123ab2fa2570a0a8aa17637561ee61a0c13f0c0805db002e4d323db76489a320e27c7748c407d7c266740861a48238265b8d576e8afc0e93657ebb162c5c367e195c7719198d0574cf72684d442ef75041a186f3440ea2c866c75562164610eb2e8b2d6d9fd7acd3f9266161ea42bff001548625a4fc325ec5c713a27e77d771b1eeb5c7d08609709342f1db57b4eaa94dc5997a426d64aad22c154dd4f3bba760ce05fcfbd9139eb5a72b9afe62d9cb4eb6f0422db9abb15490da972d5fb83c63e66b61d176a6be9682905d4a952d21dc625289ae73c95e3e5437dadc8d004e3d2d0c38590300fe45b59feb3f5991c5953afb51972798a71a8176d04df57cf2632a50df70b11096dd2ac2cefbc1a3b897abafe25b7378746ae5857e3ec342c7b76861448705ef971bb1b2c8a20cc69f6aa8438ca76b283b1e1b19f7249f0d6f27d940e316cec8968169c13b84278db43f6be21b1928ff194212ae315bda84d25a9e784d207cd7d4a447e416913ad567c784b7a6aff7e965810932bc736536362b1aa608d3fc3c3b0e3d684cec10ea21afef8c20a52243d155a251e7c1e8021f5e0f0efc396661b243c8532bae0515ee12a31eb289aad5036f2e1fb9a4a3abdb8cffea705d7c0949fab1194fdbd55252f278b23067c97f28572a4abb5b08dcabf62381e58b79137b20a16dfc18854be1778794888f73b53343ede4a49adacc19ec4de3a52cbb3c54e5b0186feed459b3869e85420100a9def62509dacd3ac8092617479495c6e6d784446bf31f65f7308d49c6f5e5e1963d83897f7cb2e731bbd6d575e5c1db668220958727f9d0dea1da9ce1c18f5222951a051b08f7f44ab86060afbaeb9bfa02b320e521b7fac7ab9afd2b8008200c2002f9cb869be6222920a325145924e36e750d9d9d7cf27d19564f3d79101b6ca193994eac7dbf109fa2c1bc91f87c554ad68c6a9e9243746033e751fda5a6be343169f38b0b054288a67fe0c2a1f2185f27f70b197fdb17cba2131213165e9f611e8303e62eb35ab06d7bc936fd737cdacf6b12a5842349841437a6884f8c0ed70b255298e85ee47c95c32ee08b800e775d5ce5d8f689e093e71129a3464dec61f00a7726047b28cf67492eb76e0524c74a50df0ba7bc5eff21673b43e41b158735053153e11c25d0f8b93450298c1bfe892345494ca2c92914118a9d8a82b3bdb297d5bedb35829147efe220164515aa2963f844b06768c432de6f9eb3744ca3eb9e7a8b2f6ddc00b88bbe6279259e070552e87d49889453479f5e800d1ac34764381f6d93517e989286224f1ec1594e97e1301b666f7e1fdb04c434523a84a13f1be9919a71557fafdd9db3e0ecde640d12f5c20ff9a69b7c2f2114b246ab3d412e851eb16d154ae63ada89733c83c3c4f9b20ff03cf4d1daf68d258faa462d8cbb618cc8270b3c98bc3b996b214cb23ec9dfed1b8505e9aa10fe84a689832353305b23af4b7efd62d33917687fd1b2cce54e6b0068820b8471d903456ac51d6efabcb52fd157a8617940f0aa83e5b7abc05f32f863448847523e9fed1dc9e1553b2f00598649963fb142962d7e9a229decb063a10e248d8c36e7b353bccdc5a1323614b5bc2bc054b2fb2092d8db4b9af48238d011a8e599bd31d3dc78b2634c51481ef809b1b7fa323ce505bd3ebddfaee0d22950b1dc07ddd899bcea4cafe225a06ab7ad6d5aed62c37c1a457b5f752e5c17f279f5ce7e6d132d148b2747cc7195eb3f99d560e3a91ec9c9895bbb41b190bfa05f1e7d5de8f67663c88841931193a493ff0a76cd9129d06fca2c8d5ef65d30bb3e71cef8a0fc3995bcb12f8d8e4834b63fa722b9bd358e211be73b35bdfda3ac14eee7dab768f44833ac7c3704ae7124e7a06bf71d684663e11a11b87bc55bf1d379b81cfd46af0ad4fb57cd5c2213f0b279808ef0e6d506fb4ab59d07dd1e67663ce92b2e9e1f31c60ee6d9057314bff8244eb69bf05f7aff6eb7d2d966669d9dc4b87c732c1d37f11ce22ee497ef99e844010062e778c4ce12114eeb7a1d79f46d2b9504c65f755972d8bb885e9b60965766c0c4208e3fb1744a75f39a6ba69c69c750bb6bdd943cccd8bdad8a4ce7e1ec408ba8b4c2228da4c26ff9a710ddac285888641868ab4d0befb7bae2d500773b8593f71923773f33351c9589f4f2f917e0bfc299c5e9b2c56511fd74f6fe072769788b22cb6a336fd4d94fa5fd9b56f9bb6fbc2ae5084f934041ab3c34231045d018ae03a6b0f7b5dcf5ddd0c96484ea899e7ed040ddccb4dbb24295cb8a4b5b583b83c9d2ec83418a5b026c9c81101f418433737c11b0eeef0cc8a1775288f0d6d8d5990219389c671eedadb05dcad86edc7f93f36be0ec99944b6c6d4bddeb23eeb8f8fbb8e3ce497bfef108c79cecdde645a238be74c0942c3a87f26d5525061033853527d3dc0f68b427325628ae2ed434bfdf40ce93f3b2cce42ffd17fe8d2b741091bd442698f55457c5e92e9cb160588733780d9c91599c9f2766ba69a9186c89061f969fb89b2151e4396ddc5b911c46be7eb8bcee2f4d8142cc0c8c5e7b7370ba46cc9ce427c5a8d2a2282c44c3ebe083d72713ee4773956cc7f9c1296f3cab698e5785910ea835980c6ba3a74ccadb43c988fc34cd8de4713e728ca09a0ce7076a3d37f55bff54b11b842c6e770670ef0ad327c742fa5f13f743d4ff00cde5082e9c2e1b13d5b4713ec8f3f57217ceb2887eeba1f8479e496e43ca6e0a81690d2d294ffe3ea475858383dbbbc650a4c7205d74518d1fcc7f8608b4ed4c2ae257bbc6a6618e15683370f9b6f5236ea875b9f639a28b130fe82f8da07921bc8b36acc39a8596b137239f34b6c28a4f011ee578bdb3db98576c3626475e5ece253b91b1972bbdda718be068becb79e90256c75923adb8e3866222f79deb2b8f778b63721c3a9fac9ee34faff208bd3ff3a777d1dd16d0d022790d72f472b365079a2063a6b49651308aa0ce468e502a80f2579a5830d66c1777276edcb8182b49152f9c1d19ad6aead5c32c12534d2505d06a899c3b16978216a67550b94555ac2c936ce7679b6d99b9a4dc1cb265186723662b6a36791ddc8f1fddebb731502b981e9d946d7bafdc5dd0627afd69c71e409452fec0c63c5c71ed0c1982b2e8577ed616e2355264107643d6537e7a47010b912d82605793e8d6b8f6c5b1820ec0d0f916fcfba7bd541c2c55ca9811deabf5404c7f8b72e4d57176c00d46f226564ba349332c32ce2da119f392f303c3aacada7792221c4cd0c60c10c71714a2f26e6a250d9a116359137d7cc645bc95cdd6e156df3017e34efcf382135eac20ec1bd6f9fd33a965ec1b1f089197153907b58e4e8f999a1854d8078d393147fc39736f31cc2e95c4103ce9e85203d941b7b59fd9bc9fef7f1373a34770c7ab95d31c7c4f19851ff547bd0f7b28267ba6235193e829a4adb0565ddccf91ad04df304eca6465d581a41662eb86696b3418b6b02ead541346eeca226666fd03bf99d11fd9d23eb046cf7c359752b7ebd3d7733c6a3388e8c22c39278bee7c2de706d5141681e0469d8b7f74c2335f336545e947931f69764a3ff7be120deda1ad9781c247d6c8259a9811f16257ddb8b9d410075a81d3fe96415e499f5466967175839f43630723d92a2e2fb7471085ceb83b0a611e23987378774814315a392c368d36bcd4bb365074b6c5803913512ad4f7c5d512e1ee0843b93be6350d0f2915f41c84cc81c7050c183f8064d6c0ef4e3c696bd8dbb63ce88685adf498136100df58d1b6eeb3ac95a8d13afc052b7d1c4b8dcb1652b704f13e44b6f622d6ad0e3c101e3991aed5f9c7092c344d2adb312583e1a0cc1fed2d61bdff4e0a34a79d1a6a6db6445c74168eb895fac615edf113e184f510f6602a274bcb96939ccd933b977fadd7407f829b6240173bbe90cca0ce32eb85d130916628fb1a2908d2bce3d73f2ab3dac37d61ec53295294265fa56c62cb4d65a8bd359832b445a38225b58ee616c3c9f70e31e350c9c3a0eb4ec996bc3847cfbaa417e91a1ab6db2af5db968d60a9f37ae4c7edac86ef303d6574aa39573002c6324719162216f4b2bb22578a2234353bb16c93fd02ab08da26b1f7740d1972fae6a07cc71c59cfd91ad9275ca91966b1e88cce1f9e3cda8f6897863d3a5594978a31cc1102251f11033f1352f0e7c5a15ea909dcd44740f9c1f6c814786ab7b35107e76b0b135155fb2fcba35751face40103827e2dce6c9522e1c7794128bf596ae0f48316beff786608d50a88290200c3a0ef51c522827d0f3fb97910c99c1770ad3da1b8e1356879549e837313775632b1478f36226223af4803878c3585a41d3477c85fa981d3d97379e581b21347b824260c43f1d760f96e743f5dcbf92268c335835c56e55c635d140a11b1651517d5bcf0690ea0e3b0987811958211da8ff858ab6ecd31baf19d4c60004f737138c3e455f6976dc30c5f877fbdbd9c998591bee025d4dfeddccbb6a67bf8a12d6d99dcfb19eaac915dd35ba76b4c34c2e0e756906fadd94c9577973260432c2b8495887e255ad0f8b68f1791100b3117e4a8f5282f5d0c89e959cdb5159150998616eea5ba7ee8a1dd0546c13cb4e803106e00223a2c96edf7c3b5ee18e7146f000486a195ff90e346e35bbb5426389be2faf930df532e8eb70e0adb25702a22e1638e218a0cbd6d08f7f20f9d051257f38a475c54982bf3a75f303f29213b1c8be43872f59ed52e6df8233a3f6bc674f167674e26ce1b87c5a0a8dfcd57d1aa9efe18f474b9bef8e88661f02110c154ad97e4b70e1cda8a1dbce92b35ad9a99a351d39ff030e28410d195f648a2ab2db62047acce45366a83973251bbca062a056df1769d040a41b64eac52937d2b82a2e748312a7d0dd165a6026df34acd163389f8b07c1ba9701fc834c96b77d553566826f341ace8440920ff0dd63727f5e21eb39549ecf79bac6133a766af786d5decef15b2b2a1291cb71148229edc3f9e867a6e4671420577517c89fb14d1506370b374cd80be2436cd58533cfe7d64cddc0000d51e7ef171854277107b4f7d6adf899083318d628e91b7310170be55bc6730b0c1e0b269ae03c62d92c7e50b203208d99827a805d0df2e48debd34a8cf6c734cc7705e44392e779b40fda22d1d497015a1ee4b89a1a6d3f5a44935bb35aa76244dde01fed03c39908a0b8fb1cae52ab9dae92288e4df3b4b57d35f17ee93148f7097538917f6e3fc1e7579d663f7c114a636c7db699bb45dad19c5a49b75dcc7f7b770f6788eb97662b064649654a58930511837c4928933edc51fdb4283362642a087259496690386565d8f748210f52db089a4838e9863d1f13a4421cf89c6c9cdeb519696b4ce827db6fb6bfd072cb1bb9dbaf7e34f57e2e036b1626059f15c07f10426f518cf2a5725a40051195f683ae44ddf7b78f7eaabfeb33948aed4c8795405d7d217259d8c8b22895e00ba8c443629eee16ec69163ce151cd65ab1f8c2556604b7f16cb1b135ae2a40da9cb64c13504a5dd28297a95e83555a08ec6f02dd1638efcf6c6d29119e3b22df4262e5503a5c829bd1faa3eb72aba35bcd5d288214854bbe7d92f5bd8d8207c92759753a06f322a667b5154e539cc4e51683e1ef05343b7b8fe5367cfa3da3f9105c05be1ff059a12f8dc3746def17c3deefe315193928b745e0632071517b7521323fb3e6593ccd860b43692b7f4addb743fcb75f9ebdbff38b7d6c1e9b12fdf0009a5a0f733efb16c8a18e9134c9db536c0a8fdebed25c2fa221ac73eb063b3d2d087941ff58d508bde4f5ea8cdb613799fe2b97d27268c3d5c691697d3b2fa9db952dd013f24bd23d2fdf9227bc7aca90b5217c3e9e60afdfb5012dab28cf4f52d4553be897f621c336edc99dcb223163563a837fb04072b77086c71ba860538efe06b5ac14996922a88cee08228105e23fd0659e0cac388782bd9534eb87d80d0c71badb6b37a8f1cde9068c4a3c3e817ff3f8238ae1b07fb819e5a7e45efc1c369163c751538fdf6430a91ebc6abdaa8a5456d824ad143c95c1875037f65f441b74217a835d60d171db7cc5144503cbb4767bdead67493d63a0325fc35c67425804d85ed7d4f15e0158affa9ad83dab235e0393296f266dc16cd06b0aa8be5a5441c8fa34ec7159e07152243a8c9225d2ac8c6a303b0b93c73e9a5ef0373b6ecc9f3b61abe2d2c8dedac2626f2796337026e838d4dddf71083013e9f4ba6ae2897bb1fe04c47a8111fbf2de5e580fbd233985768e7e844834d6baa076a3db7ac2a8bcdf5d2ce14d3068b458f48c7e76be30e7a08ca5bfa295ae38d4a139b6b00e84a86f4c6cb227fab5d58b448a27ae4bfd8b0ee315e5f2dd030ab89c9b4c67e0076628791f01275654f4f606796baa06621d41feba552435bea689be3ba83d22c39ac8b471c0429231b1b338a1211a792ad22ebf99e4fb86f1266620efeda3b526092c3ed2511cd32d6288c676b93108537886ae4a3799afebbece360aba192c25d7ffa2d34d696b7088e7aceafd75e173031877ac7a42027543480e64cb7dc45fa5f632b45453e4c8661b06a98b3b02cb8629bb09fc3bf84e27e5af5dad1b543ec5668b80132aab1758c4ffd721dc687c9bbf7a59c17c7ccbfca297d5b8e59e043607196bdf19226d3e0054ce62c48ee852c28394e355b4a4c4ce12c2d84f18369460d828931ab27fc7e0a64578a5374f4d609c4394b0f9a1255c9245d16ee480f47f7b3f8c86cac5250162a99b10493e0de85d091b0f8529e29701d473497db2e4420815466a93f16ff6964f256dcc6dde8bcdb96c8ea543fa3ee2e817386927f611df3c5732d3076e7e5aaa596da93f5437275ab57ca7073475f5045a950e4c172090874a54b61bb100c58ccdf58eb3b1d707c17a40e9196158d83f991a8f0a6b9cfe08a15c61db38969e0e8e36bc0b984ef8bf501e8cc20814d6e8261ff93435526888d4b2b02c7828b25e42a246eeafdf9afd4e43f986f32e08b3025a4f89b9d3c7251224209272e4b6d5da82e21e477cf360ecf48f16d6a1bae2a54a14db0055635eac1b522e5a8b3e36a880452ac870bdbcc30e36900134fd33a9703dcaca27ad4bc3509199697ad028b9788beeabfac776e262583dda0d65957694b96b7d9fe604e2a78b42ea6f82eaf0414c44dead527eec764bb304395fab36db22ba159f9b4daf0d0ffa65ffa3a70db79ccd40002d1467bcef5a317a68461cf1ddd663077a150c4dcfbfcab4802d4c0eda38f2cd4a055959d3d17ca47484639317217959ce80885fcc64fc4a19608509ad05b7f8dfa36fc2a0675ef0744d9356e7d2e359ecef5d049abbcdcb332447fe95fc8406e7dff16beb13801cad97eaf05befaba35db120e8863ff8eb4b3fb6446e4a1018beb4e77a715400004c304ec88056aa91542dbf96715df6232af430b69d47742afd19d2140cb045834b5dc26446ee051503755907cd4d8da9bc9013b34f85473389a594c5c4e3e1f07d95cd523668d142c295acb12d85882cdc898f0f0faf5767725b699ecfc0b9ce7645f0a1fd8809ac6b62966a2fc73c189c91be145c105fba2a91dc78f0667b22d72e4541ea640d55e909593ce37355129fe4976eb28f2d8195b26014a475fd703fbaaf6578a6bc06356ade0dc7d05a54a860e84b38ad16b0187c6e0d36d81ce9222a3cfb4b77ae6706ffb7b7ea48b25d0851669e45aa4399759a2564db858e79478459341e6d2247e26f43b602836ba9bba72a3d6ac412dc4e9f42e09e2a3edf578913f9cebcfeb6d17f0aeae96f4fae2abc852db5da737091a37bdc98df5d77fd5843360a912db8269b81d8fc9a1b3e320065caab6dba343c2f05315208ead90c2426f818af8e21b4fe5b0e2991beeff18c09887669a05b839b7273eee657209203823e643aae4e5e7786d832fa174ab7a2562848d043442d324f52bfe670c6ca4aec8c5ec1407551e757dd76b79ba0bbcdf7569bf4441f4615419a17b6ee3b5cbf70ef0e6d1ecfbf04abbc2746c170e0416dd0959738fa4d574acd5cab83f4a174a50801792070d9996a317929e917f41d98d4be90d0c79170f968c0577fde09af3dc6728958a048a795f08de2461a128caddfa2c006866b6ec56c5f2f81facca0a5793f5a207587d15b9cb2cab52a96ed9e87c52f265dc3723f92da4989805fd5ab2c5d3ee531e6d53c44ba97d8965a85fbacabec5e2d16eee5a965b67835124ca39f07717debed49daa478a7aee5bd5b62d55f0ed877356e7139dd33f09c6f6010dfe923f0ccc350cf014bc1f54abf1b93dfb0c0988637806b788f403728d7effa0ee9d2c464c64fbc72a0b8eef79f6e165f0c3f16b3efedad5abd24ff705502c2d12f3e040379b4f9ed682f245ffa5931a292b9d0a20b5a09ad49de7356d8f414ef32f3ee8787d575639be84681dbe6d4234cd8737e845a33b47643021b48db587e4808df2ddd63dd20e3be5128585da76a7ef80de3e330e6f53ae92ec43aed55f2b1e01b81b73a0f38f8124cf698bdb7e59750524e2ed22720dc2db5caf7a61137614f564ca34642aad7bc04ddb84f7e80a8919a3fd11b120685eebd10e753ac8495725d24025936a81a01dbc5c693de54b014e062a2486aaac53c3f127f077590a2b931b94e436763d6c5eda2dafe1c69f2e336750e66adc562f2ef3f5a47b567a035b00b09fb43032540c6a9451d50b81fea96dbaf675688540acec39d1094204dec6becd33200fcb6885ee7f9445395f73bfcc2b61e7e8303c82eaa5a44c51078a83b0f8cb630f5a9989c92c9fc3ed92ef2afafd14b4db5a4a0a8582a70c47e820007d510ad270bfe0a92783568835f5ab90729764fe31c0ba609434e6cb00602b823e292d3f6798349da8d7b18194da9c654ea9808c00d324922b5cbd2a1558c2bb5c1646794a05d1f3ee65e4105917de91260640273920736965c96b30814e916919f0cb9a222dbe671e1b22b36948a01e0614e3cf1101c3efa0f7d4d8d804180fc296242cdc3df3bb8354c7df72f073038cb6b6d5181cc4dce1d23a6b79ad8c5984671cd4472642bc0815cad0bd579ce4a6265857068838d2300620c55b0d1038c813e0c2f861f2a8fc5a4b2fe41f97f8a216b5a64fc7a476c54f20edcbc5f81a8aaeada0b237c2d158a7baa5e2f38c9eb2cf8bf6b879e39886e42b9f9c5575b0e9137d85fbad5360be73bcad7fc9c6d0bfb2be874ea5bb5701c36a976b382f58f1e58f19d4611b0b9fb32ae2d7ed3f1ba997918f9dfccfc93d46039051cddd1bee25668b46608085146672b32d796a7a17a5afece6a8abb1d495827a568826ed8d0e903c152fbf299338d25419e4b120e381ea6881ee1dd73578275a40eec59e115d18ef7ae3b7e63dd4a0a8de0c781aa03fe16166b210c8a691a4f9b2ff9afad6e5f0731d09822d6a2cb8402c41ff48a25406101a55de30f8f76ca6f6fc4251236aab0cc8cb57105d24ef55b861c5efdd2da5ccbf0de73b0943bcebc003034c9a0866a01f08389925cac713d82714959a05227ffd9f9fa84c22643ee825a47cfcd068cf72192807005ab48d42c80cef9af5a0e6a07ca8aca2d15b36555efaf2f7ebce40205580ef5cfb830493c090bbbbcfeb10993477160aa07d641539380f9bd455550920422fb21a12dd01268b4609f541bba89ce8c448264eeabba75653bdb054dd7e75f83911c98d431f6db2f0372fac82069718c52a7d5d48dc58560dce7e7ced98bfeeb90314272912dc95145f343287c27397d4ccbcb1004027a2ea3a8064c8db7a33aff4994879fe8b0c093ab83c36495dd4465023b9e21703b5be33c1505b6e7dc8cdda06b7c388c8ba74f87eadc2148e5bc7f022c37633e55dd83a9392c6c6a199b8a2745cc3b25344d6b2e3c71282589a5678a14be9be98d8f1fcaab9cf786858113dd49621c8fade18948cfb498a8df1f1c7fa46ca1f1bb37ecb6ea3a3228e8db9c5f853e6c7f78fabca9b6e7ee21c76d5564757e2baace967439ccb9f833ae1c35df999c46519478559e1b41af3b9dcf27bb6cceda3021942e0fc3f621b531b4f2bcf3f076f0f3bcc15dd0c7f10d96af9c7127ffed6d7d90f9e49550fdce43d75920537bdba345a5124896cf086208d03ba9213990d229cca0635bb2b8f05d655b82483f17cb83083930e267cc081eef4032b45d59dc27e673e1c6938399cd828c0d2007478ddfd9922973d0e62cd6ddf1515687a7655d33cb7a2ca4d48e1f0e62ea60b2773740b0327b1f8f4b86d1b32a2a7c3440379ee1916dc40dc7b5467f29a65a142a414a90b24a53ece32bd7e7e3a7bf0bbba102f940e5afa56e27cdd08e39fe483fbed3eb6a6eea9939264658fcb3fd21344b3db81ea993c8aca4d63b20b4c9d7125d10c0db8591bb8a7427593e13d9a3ed38dc3bd9000b093e109c4449c8d08251fccd7a312dccb47f520454308851d70e7b78405154aa5c1265cb87442d0fdc4b3caf7f815fefbfb9cf7da706afb2e4ef35ee4ebba6f316bf7dbb278df66424a0121d619330c0082b1d62f98045e6ec5d2979c98d8883392455428b1cacc862f361e9cd634f2b8187b73bd13f9e70c9df7eb39c772aaaf959c84b7e55401bdcfe92ad8234d9a618b48353d022fe17667005eea334cd490151c2163123c9ae2087b2c5c09bf429bf99dffc7118aee4b8dfc80dffff23edbc1597c3e5b0fa810eafdbeb64d44d3f8882857329c74133ed55e0b12c2667a16958e736508b3ae54bae099dc2ef93684febe3a2759dec26e6af9910aff4c0e8fcaf77bf7912f870e217191c6db0094595c80c74a1bc8ba780d63140323e5d1eebeb5fc9828ed2585930e563733244b8537f9494995f2c319e5b679a52e0fbfa0dcb33a5c6d49bb3139f1692df4734afb5d785fb431b5da8d2a8df27767018a64bc580598322dd4074e463578e8e2c92cda75aa1ff4c5383e2317abfd0f39192e5901dc92290791c54705ec662c546cb350ca165903c9f0fd03530925eeba01cba614bc75853291f2768c2ef1db261a548f74447e7865dfb74003b7295c44ed398d0dfc2394ba37b4432dba9f641fe167e298f2d6cba98e7f5645e0837d608e08303e63111dffbe917562e3a78810aef5b52c703a7700da936896449663b180c2c1e4eeda13aa75d3bc9c6aa07558b6930c369ff994c14c3b3879c4271d2591a1d372ccd3bc9f26a655d839850f4dee00d6260a4267ba9513e755456ec1466353f97870bd3e7cc6863f1fb9f45f756c100f63db396e300a1e51b12e0eb96b360026dc1ee9bae2c23b2794f968ba8f9fbb74df260a7ac28f0627e6331a94f39b06c3798fdeb22dcbd49c3cb3ba63c69cae05233382bb8824d0805a952a262c5be6dc4f6c2373fb0dd5ea16a4eea646933811555ec555e5f623d6afd45964e3adb4b7664bfdbd1bf8773bf306bcb13aeca6fbd4d47c34a4a380b71eabeafcf21ea44a49a6ce9df1222cb671b899892853cb0ade95c44fe12dd2977020f1f8cab3357eb02fdb399dc8c0e32418ccb7d6419d8d32f7d5690720bed451052a0782173edd5b7b8f5506fe1cad42c440da305e4c40c2b2903f0e5b24b7302ecabc706cd9b8eeb3fd64b77b280ca693623959622edb9f0055f4ec1e16fcd2e2f851fa9c02f6fa4e644f7f39da1b9db5ad8ee00d43b4e91f993654b5cd9a8a6ae4d3fec77ba315b60d28f49884599b1953be09e8ed47e22ba0e9f117d25da4ddadacb34eab1466b2634b4c78fa812bdcef0f0a03b8d5fac5067c387d2702339aeb3e7844ba56028d4113779ba8d7736007c39b6e882ed2e1b848b363c1d35be5e8810f3e0765f9e90fa17d85ce6a9d4a2066ee81c9a7a37d796d442043bad8b58de5c64a3513ec182782aeeae4369353d49f34f7d49b5c0bc94ffce54948f242910faa641b01fe135f0ad0a4c156e639d2e8ea9d9f1f03295dd372df03d8676c8515a69707ad701805165464a489f277955a914216c5413db4f730d4e496803dd1beabd014ad762aa3d6397b5ab3f93c5ce4ceac6de771e4dee71517e82963720e83934c788dca10d0e6491128d6f0944ec4b33f93e68de8987866da05670c13c33c778d49638b0db5ef45bd9d4904eed96a12f82a1465b892bf4b4d763c1a0eb9a8e2a90b65a008a8e6ea104e36b4df2045bd157660ffee4c1fb1c2e58ca3445aef45a33104efc1ab1be28977553338aaf20da0e308d5683ed7868fcc71db0986e8f79e246fed8d4e05848d63e69e77364e7a31e94d6afd79b38e9955c9c0268e17fee406a8030d3a0ba8726c3082c0a4651a504a03bb0db01c1967962249d3e08f1645b98d27e84df09a306da9f100753bf3ccfef9ae2c8679146ce5e154974d35e247cd5713e63fb26c4c7ffe56e572865ef6ccec2396910ffcaf1da45752b69cb089c8a675463da1935e84d3e7cfe6a3f5aac64002f2afcebcbd12d5d591ef6d834da30ceaf720d4e62aebd135f21062964ace14832ea32cd38c007590a9fb0dffcd9068089637c8ae658a7e9df1367cf6e5a5125f001cbfeb33c871323a22ec6fe6cdd11ab0c163e4288c1bdd4164bdb1cf3346b5defeadb51311637ffa054dab0b634970fa3005acc9a1b36d1483b0bce2ac44939bc3ffea35ff3953ff53abb916d91da293e95b4659c8693e7ceed880a677e06b281b52115bf56c587f3285c7397bd46447f93ec39293fd3f0e80d5495a3228e695f0b97b1a97f760d94e1bf712f3814d32774436339e6eed48f49c34be0140738b1014ad08bcb4701e49be4000f51154764a4dc5788faad1a54f1db20c846affaaef60ede42ad72da5e0c579bd69df251343a736b293f07d3e6cf8738b212d4f564f31c83d81a0ea8ec8154647208d8400c9180425685277734514d69c385e227469457e11e0252c17d52ad8be5038b0b1207f812c26847ec009b09ed01d07024c9e8c705bf39f84bda07a7bb6a8a213333175ffb8239d645199b574a48f8474e92185e506fa704ba418e9ae45aa2bc48084737570136f72b1608b961cd487ff82c33770f7e397d09c003af3d1c18390487ab8da3aa0f3d9758fea19833e68f2a71e7fb7495d75212cd606a14a3c4a9cd572016153318af17cc64dd796913bc44943ec75e8df6a4f2f1aba1471589f0cba7beac472d2f745ed282efea8d518245f0695cd7d312bb67396469f3e664b5e53b7cb99e976b1065d09e146e2998c45fb0944c37d59a4872da179fde8d390c2c7ca54e0b5c4cace83890f94c601de4ed15e78595c638e98ec37f338c7091585945b7a0bb38ea63908889c12e0551524c1f857df684e11e0e1e4308196264befef6f1986bd7550766c2da4b1772acb56b6bf2f78b30fa90813cddf519180a4bafa0c597750a6079394831b127c3802666a412e71c3e04a303d34e28c6bea0b0f3fd9ad35e453a521442163b5e1744ce8599513dec4498427c4d98d49f761917844b601a4c7082b004778d53c8ce82fb8f9903ef9b95fdef13ed8fb172b5d7f97883c393e0db71c9a4307a11a9c3e1c6d8a991b8cbbf9ceb58ff66ccd38344b510a6ba3232bfbfb0dae14e13890f8403150dfdf981db57151468469c97410d5a0788a17a64c0690ed456e215912eba5bd0ca53f9fb02094baab10b0b2b11183c4ca69d0df1f0d0ea0a4c86548ded79de72717813a869509575c54dfeb093e660b0a0c5cad560c55d5472ff3f02fec0c68e1a2e9a57a2ab64bf830489cdc178a3a0a6477fb9bf0a0af43a83d4bd585d2381b679c91b21c595a62f05304b71a65de555c01c30713ab65b6f7af1a48ddc71e5537ede57ece57a460fc9d8f90dfe3dd4c4e473bfdd5297f9f9605cc0f78584a7e97da7ce2208ccfef494155d26839313cb6ba63b6b98fe962bd881e2ae24526d7907e177944661b56da057d98142ac60ed2892b870713365b100816cf4718459aaf876ddf6c9954e2d24782a775f711184747fc007dd1f40d639ab1e157d3c0e18a485bb47d1e5894dc73f7d158e8e9cc11837fe8ea96b7bd41155ce136f014b345e547322c51c76871f23b379d273a39af604006af19054ddc48d7d3b075e915ba68bb3e89b0c6839105df2381de1e1d93eed42cde7f4448f31e97a21e4a6963fb6eb827fe1c6262dd88a944e53f7a49e84d69052bac990032141290f3222ee431379f076135861074832c56d94412b024c3244eb93a62fc1c83976ddbdbff4622c69219b6945c6ab07a9b75b6d7afa2e4887d80d590c17d9f26b472723aec04bb594d4d08f43ff553a6230d633a0bdd9de4543996b6b157a98d30628d17d9f3484e6de86f8770d28ca35c0cb726170658ee40d3c0005c5c5e38383de87d50a9ee0c09fbd62177558e163d2216685e3bc2b4ac7e23bfb5ca8ec95de8c160aebaad2f24a616ad3a97a6c58b8c2cbf4c92bcc1e6f32909aa8feb38fb308859c52e8fca79aaec366b349ed4b6e770a668173e8f707ab763bdd72f3d7fc5a2507cbde9f45c482ce845053eac0a23c3a6ba1ee91e3f6da295b657d63e25279a6a4667b561dba3d087da40c8452b22bab6fe980ae9e4909e52218dc273886931e03b7cc450cd2d7a572948f8aa09993216a59f26bddf36e2655c12c206911d5f9f552b40090b6cc08e79623cee44c9c6c2fb4cd86bfff34446f1c4933a5d18df950536d7a8daa2c58c4f92310e825dbbed44f0a47655d2c5eb06733c4a29ba8037cc97f28c225263e48af99f926c10732eb1f89d969e94ae82c5615ebccd17557c4c901a27395909339e3bf2fe01b5725e72c8dfffbd7c37e16b418ab55f7ee100e75d5b948b651b039847eb3d2f6acffb2700ac24f6cdb49babc678e9240e574e931a3c56ce1889601dd8cef226144a2d40cf1c7f39fe065f427c04e25bfaf1d47befce29e55a0352552e630fcfd70b56251a4aec1faef51d08df1026bd4f341efcdd8d0f4673bb8efbfdc02fd4e056941c9963531027bca7b42ffb07224494f95c423b7d294d3676c797ad3564fe2ef6e7a13c19e53bc7edc0ef293bb2ef29a6f83a8a95b39dd4e630733a78e0d4de998f92d413c1bf515c70bac47fbe82a529bfa3339225d8e9877b86a9d4e8cdd739f5b754f16836409665c024d2b97a8764dfbabd739863b232a1b4e26655c3c8c95ad63785c8505f2f7d6ec13544002ebefe00aed4d2c916ce7f6a773b4e4f6def99b5f731836e40d02f52395140d3272c659250d6befa290443339518fcf98990f80a569362fa3a1c2d65d7d081943fe24de2bc0a2d9de81315e4212eba607ae671c3424842a7c6a2b8664ff9dd070efe1a9a098ca2267cabdbaae487adb56082ad70ba6fffc1749559018f6add543fffc77190061b8a002703668e209bf2ac7e9dea9abc0d4273b0130180001760f79753363734c0bc44d316318256f256f98840cbd947b604c3d94c632931351bd7edec3be5229ace478743f76227341edec28da1d3664d91756938d51f264369ca9218eb12586e4663d7829a47b5b0b956fc6979850303ab15d61333ad2303e5235ef21899fede47888c02e2db28fea4ba213ae74ecef8dba110287090b73afb5d8b504b7f6719e3b4d8b8f219e0dc2aae2b4d9f91d026a04db473dc603e1ff21dfd7de4d87b2ae169a4a309f8f679059744df36ad49d61b055af0746c663ef5cb8d52d6c93bcd60db1646e7b8742d769c13d5ccd7346f81feb168380084b12d6d69474754afc828fe05062edc33712faa1e7e0ac7d4f4a89c518ce5aa58be2d7cdb479806260821009dbb6bff28ef9407de379322d29c564ca5569224480963c80b95ac37067b64673b9a0fe4c0ff3b32cf6e694ef77169aa0338237ad2dc3d10ddaec19178401f953bc60fd41d07d106a6287e269c091214ed347c8b374b46f406c4a67f9235fc12bb715e13a0a1ffaa592c33424d30fce25b86ee0a60d1dfaeb07452b86c8618256afdbd3592d0f259304f2a8a824d11ca83663ce19985fbf96da8012b4084757272b2e8bbf210b8509308a56bb3684864097d2f35c5d5ab20f98b1efa9fc915483034c82958e4fcff02766fe34db9256049d2dbf5a04a3d18299768d0766a099a8d9332c7a9a4cfb83c427b093d16717bceceb0097aba917b0e3d0a1165b256fcc5aecc4e1e47223778020ad99b05a353dcef4b78a7eb31d5097f8458a73bfea2d2114c7d8bfa809ddaf64a00f4c2f2ae704e919091d151c28e85d59127e24425342d4fbe59a7a652d3527e2b6f2d989e3baebc05dff1c8adcaa5c2c7eecb9af605f5fa88bdae55ee9a70836340cce6ebf23afeefe4dca76c60aa2d4fd7050ed6a54c716c2884986c98cbbcb82c9a11f609b305455d7a85f5d122c73a68c29c65d333ce4829742b02a332f2dbad7d3ef74c1b813f3e5b6931b2fb1689b63f00314cdb2aa3f3c5f33d2d3aa75ec2505e40fd25042e14adf35a04f021b7acb1d1824befc79d0ed9654642347e580ec1071df831c689b9a4ed687817a87fdd967ba7a054ca2969fa1b134938048d5c3fd770411588050a943d23dcd1f528fa9f334c474913b4eba831a5803804a5211c6448c4632b9d37a701199e1bf3dee6895b68c9473c7d5615c7e6230b2579ef1733c3622edfbc4bd8edad83fb52312163aac361a8dac6eac609ac699f4609d19b8448f4771aea285e7678689d06818da04c46c2e3e1fdcc817677d1dc787551eb6e0150c8b6e33f5400cc3ff16cb7362bb49aa180ad5a9bd3f49ed13ca5376a55895f51374cecbc9a4ab585113b4aa26da445fc0656603dc7cff83ff9e3f4623e0999ed6567c2c27e7e80c6cac33190ffa39f844e5bd6756f42c8fe824c2137a1ad52b4b8932248dc71010b915516e9769cdad471a96dc70a09959e1b91b20a42dd847b777385bd9c2ff39f3d0ba1040f93ebe2f5c61544493e725699cb29f2320c16c245f08fa2202ff9d22f296cd112604d218f2b9852b4205a7624f1aa8222b8a1e04126af643d7ab9da6b5030ad4e3b91a5df3501ca2ca115dab1ef951d65df798ee3b840d7a52ef3d7b003b2b7087007688c70f1d48809a3eb6f5a6df32a91e076ac23b506ceb86ad8d52f1cb9eaa8b9b1ecab83967a4b7f3ba95134aafb0ba26fb9091115a9c9146bd5e7c3f9746fa16732fd6d687e9ff48d2ca01d904abea5a07f3be35771f503bbb6f2a8b1efbbdd92009710da5b0107b02662c45f6056244681b48897b3a7931e87f5587b6f16e284fe65625a070b76999cac11dea9222ae39902d3d5a3756ae22b5fc3c61e001099e16773a44ead66738cf8d18f9e35accbab01353ed7b2c0dfe075fdf24f9e404712dea82b6a9cc2accc7014ae4e4cb948348be50cfd4377ce84683984a428da9ab9e29f37af4027386594961ae3d165e15afb0e61d0aa8060280277891820a2f1083e48cfb9eea500ff30d444698c53a410cda92968520b040f29fa00eb98bb58ec1d79d4e10a1725c42262403a8dd9faf5e0e727c4f01a4df5c17846b9e6fc32be21e36bfa9214c48b5950af0158e1deafa58cff34a6b0afbf91756051e19fdb3486df431c4c819140b8aa68bcc6ec7ab48f57b3946f89d59752fe72bcd57db278ef8eabec44172a7459adbef48c2924843076ee8b59d6f2973440021875df675cbf59fd5f187f2715b5390cf5e02edf61b520e37b723e4596ed2498f3dda4d8adcc4e18a7271cbe097526e37ec13f8c2021fe5e6b2e16c5f001dc3c434a59405c9ce675c6ce884af0300692bb5b6894eb6597bd5ea843bea70ce510671427920ab9345a27b17584d79684cbcc740e691cd70f1a9ae345e7a08d04ba2f82d80d7f2d7122ade02c9b8a5121c5e930014e653a86ddaf7c7bfa99a99ecbe8e688869c2c187b6a958b802057d10c3bbb3dcf512e963b6e85f47aae124d5a5994499b0211d580e1c454a267ec37feb4067a910bcd187a041fc4d9765f17965cf7c73b5d926196034ba791f96afee797eaef974207fb96be65e0e2e23be53b8cce6d6170b52fb956a949d64c94052ae53162f60019b16634f0f08448da69040ece6bf88c37fd14e28a91e047d74bf5f3dc75dcb87cc2d00bf48c5fae4221d222bbcc6187c8eeefb93aed795a7e42d70b07573f6a21921f14fa8de09b849a58d0a5e9f0069a5b1d44edb25188238792ee2546fcd33c3e060753589d4b9314e2d9b06ee9067d0cee2b6645a95f5db95da1b5260792d12a3afe79f618401baee08d3529ff9aaa4bdcf3ea7befdc67f6f0a1802476e5660637e66957ddda5c7deeff1d2076ddb994c43af53dc386fb8129d25eb2e48218237ffbc8546a2873115e4798e3c5cb48b43ca6711be3189f27a57c4fa18fd77823db1997a9baf988c1570750b1d8423e1c3d858ad95d389b81474ba2603842071bdf4b60afcb7e8514cc9a4eba26c8db8dc96d4c9587b249fd1054182132e716f547bd1541f7ee77cadd59f2fb83ac7885469a47f693c9eb233e8a4f4b2463c2e97224a8fa965dda46c5dc83698112808d34dec0456bd92e208c17447e507a64d417876f002fc3d0f1421288db8b75e4a32fe1c698edf5fd2b3b0935c8d6838c98999f2fb66d1b6b152256a80149c3714cbf60dbd95ef9c2db19aee2b7aed88fc83115179bc6d896b48e6a88a574e3b79c3c78dccd7f1d37f16201ef4616c847a3bda864e7cde2116ee50c3b3af995060a161d277c307867004612a7357601d3a4fbce813a01f4dad42837af026fa9892506e189fde83fe0b4ab8e59bee565a668d900542cb52e9a8f06b78263b7aba30502e86767690f86f60dad574220ef4ba08eeb0837a3d77ef0188805e9395d96f88213a41c86827f14f511a93a37a001fe9b9f419fb913bf001be795fad4af215ad683a84a22d049ee3e3eab7bf379fe03143c63cacffda9ace0c0fa7648b1ad2bbaad335ea83cf5e2876d5aa5e4cb9dc966817ce865971f86065169e8bcf5b7f189f58021f09a69c81d87d1cb907e7f66fcb0e4427a76c7a2300238cd56663e1c2eb260d7f53d1bce325c294e53691a6a03c75e3623161ecf5e4ecad713b6a7f67ccf2d9f81a3f08ce9ca1284b78b0e682d6ce1b7f2e65a927fac6cb2b366cd5cd3b96c21c6511cc45143af13f974795fdbf7de530602ff4a7883f0621a1cbd13d12af7ae90183ffe2ca687cbc738273fb6f9265054ccc30c0c9daf5a55c2ab1b4cb8f22a960616d9bb75501d863791bee998de18ea8c7f44dedd077d7dd281bf6476e6e8eb3548cf55232365e71ac03db3c6c568422d3b9eda2091015857961584dd4b1b0da656ac4d2323df8525d35676e829307fe28d45502975438cb1afc438381c6b4f7aa338d221af6f9e5d029d55287934821679de8a9dfd477fbf7b707162c5e2a1a6614a36ab87615b68d4d22efd60f16f54c4981bb91e612fb9d9506a57adbe285f4d32ec2ee3accd62ff95681d0260609cb9081d7f8232b5d8d94530af9ed9312b90963934515f3c6b33adfb0f6da8d59ddc483aca80d0db23398f2fe4a519908826ecf7b2c74baa0b4d1daafe46d88193b5dbbf8d759a5eb35de26c4469b955b4d4648e841805ab375889d701c8f93ab53dbae4fc1bb3b9c6845c07243e1dbc43ee33bc88f6bdbba5bc59acc12493faf9c4da1856c1cfbfdcdc0c3fb1c05d88c1c71acb2e365d60e66aa0a1cfcc68bd502550e1f77c573f25a2a6b250709b6b6d03baf6401bdbf961a087406e619c14e3e931e67c80e591d64b0353b7a0f181e89a22f73dfe63c31e912cdc6275bfb5ac98b7e44660ea2ac9dd530b9ef546351e05c3b22b69591a975acf21765367c58fd90edad0500c730387cd95f6925a20df88e1fd8b95f8390f56d804e9038ad11738f9e739accee43b12c1d4766cd6b0812318752f0a7dbb50017f14bb9808dd08c94cb3f7aea18de31f9908854a01331747141868a631dbd5131dd6bc934393b9d84c73f6ef05d0e8ed52ef22f1044b727751df1c8014c57eaa15b481fe3a48e86ee154ac96b0eeae61e960fc3b8b810a0b83d51cb5566eed5251a30a2a5ea709ff16e6b4ce926bde89f2e1314f2e7061f346d41428113f642d8ec57b9f7cdc220f80f486d0edb9f7970dce2a0d2c6fb3cb47cee797566802bf8894cb97f87bf33433381ed841f988060fce24f99f6ede467a49990b5e0b6d788eae48c2f10ebc719fb10b17bd15170a25fd4fada1769b0f36afd98751ddea657249c0d81e45cb4633a769f840855dcd82f02a19f9a1d68428563f7428e323baf339b893f3a6a48d3ac402687d1d72d20d9e4a9783cc3502acce2ca1bef9174134dfafea2aa625ae2f3afdfab9966a71b1efa9e451e5c28b8e6179741bb1333acec86acf428618fd2cf75cc32a3fcc26cbb7cbceda851c4379c16c48061b0d5338c06ba915bc82173093d110853bd37cd8690f184e77768923d500e996c553c41114a01199bb542b87301e6eeaf818629c6e6b0abf0cbb76993f321d2d8150b2f7aa8135a404414eb445935a1479e90837889d7bb285ec1b4a496828a4136991f7b1d7533d86f60e19f2f42f8ce452d89c81d8a3c6bed7a61bff8dbea5119bd02627abbae4f9ae1dea328c0ca914f1e6b6f859116903b543125de426fcc0f7aceef2a3976888c946483f247e35430d5f77db830c979ef7945f1b94a7f815c369614d2c9e43e968fdcece48117b73ea91a6e2821048f3b3fb4a00d5298b7d92fd5ba1351c620701c6d57148f63d574e1bf908a342eb0fcdca8fc046db606f5627ac85c61eb4e7211cf395ef0b8ed74ae495c56d6646293c6ccbb2ed117dc129d54a41333117da46212bde05d5a1c129a4cbac34736bedf53a6ecc64a2a65fd885fc6e168c59cb0a27c8f28ca349f3904de51a31e45b6cf7f828efa40c3d818997c7175c9f08372f7952508e814a53704d7391e383aa8251b0b8c8cab060e93bfb232ef33ff67caa9c13168a9fd94a4d27358d1b3c195117277b468b3e3395b5fdb3ed83f80e9aef793f1c07cf9941f23e7bfecd981bcbd1624ed39e12657cfb73024eca3a6756cfcbdfa4b316341f13c511f315961a3a8cda396e7e8e8cc3b498868006a1ad889e007da2d77efd57987938822ccf67b5dcd148438c2a973d5fd3388cad4f3c008d9f7caa2d149857a4ca081cc83c35be083b9a8953c05a864088a3befe5ee201a4309caaa4469bc4feef0412ea023bfb58df1e2d1922b31c7bad6556a7f4011f2e64dccfc01d94c2d08e9034508fd87306a7dec8019aa5474b82f0b3183494b253961fc6f1db17d380e2e9f6a719b65995e6a31ba418e14ca11ecd3267d9ee4f92afd368681d69875abf435dd4b56d632dad401c20ef8f03253b5fec628b4e83b39286a09b9e334d58eefb4c8a26337bb7746c4d97f587956ee03e1e07f52c6bc252d17fc7f0f13e9d2f9d33e8068136f10d4a01c9d080bce8ad83b4c84fa8d06658e7acd7cb644d59dae92dc28490cba3a526eeabb02c8ae716484a33c6c6486c94816c1b4482a04847bbffee3a5104b6099f78834375352234d1c8bec440a0cbec0aff0d93e2ee5bf05f9faa4d702a300375c833bcd21db300f7a43da5468be58a6e47f63afae523ba2cfc1ea5c1fb948c7285c663954b6bd4accbed12e89e54f0165795abf5f8cef84901ab110fc078e38ce5108913a1eb29d03b95bee6a8edade3800290ad4c375b4173efaf58eaf43d0630fb5077e7ca430b8ac59ca2ad4f7dfc2f92044693618701ad0678fbbd35ad7f3362cde22873c245554435fafec969df6d2d0c2e92f34028f55dacb070f669b0725c11e458579421c20c70e09ef06d4586f911e40a3440b732e61c94c814b482b15daee760388c5a0a35ca09166d01a0be4a1c8bf619685670e86489e17d50b64443fb9207298f8e713e9812b698bedb6a1e46f2536a69204fdb2a38990f08c1be10a2d21b9f90236d5142919422af06bd38818b19cccbb63f13537680049eb80c50012b5e7ab3e079dd09ebded1f514f50226ff1b5c76b001dbf35eb5a7590474303c93817829fda9d570efe53835036676ff6ea5dcec65744e8f7fbe7a78813feaf5ea01e980b4df5e8a289536d37477242194d71c06b06b07f0f2c03149e3836475d35a8655a3da592ef9ff900b7697ef6d9aa578d0a41d54059963b903058667994fd62fed24b7943905b7b82cbe58970a1af41bb5840f797dc914c3f3a13ce078692821dc3d319a967b65a30508975a0feb81d70e0a450787dbfad1c825db72fc96efbd71f198f750796c08e012532ec4a2465d54256cfa0d89201ac03656366b421dd2e74aae5708e0b9f98835e0797c7738e2e7c1a4726a50dce37032faa6db8b4e52723c27eccdb695339a1578623ec365c90784e76f54e618ee30198a662e08ec352978fc03e64354cbc44fbb344b5e38d4cc7c184f82415b71bb2df2b0e832c3c35966fd856afc3731284e5f2fb0b518f90d981dd3833fd3aa2cabb68f62fbde7f18901e5095b2b204cd567be2d7fbc6574e7519559114348e9c9382147b8d3776131a6122a7291ba101aade3ee7ba4735f0c673b33ac67fb5b659b2139f8630db90b4a1a533d19469c98b1f1514ba6bc19063056f9d4fb92e852a329feff76b9a462a4d96fdfca2e5e1bdbe2059d934f4b91f0d892f03f2717356d095b0273bfc434012655b470bc2fe2099e276da8e5ac311833ca96bb5130e4899e2ac81886d52a40f5bbc6602b24aa6b61b0291c8fa0abe6ae8651d4cd7ce25d18ddebccb4d8c29ae6928cc871af485122b22421f30a44ecf4c6b398d26890a7f63f22530d311ef686cc14b63c1c8c775db4253a6523b2cbb20b23ed717a8126a95e710f00a4c84b631a740155d8537fe8b692854c07fd9b3a92f3511d5c6da16290edd131175fbf791c24a115c17333d57e27262b281469c8bf5a1e92450ab679f693e9c26831a1f3d1549e2f9870f60d91758d14b24edbb1775ce28d4e771a5877ffb78662c98e377cb97b7ecb5e8992582765f2205a82625b910ec7c75deb9ac6d41a8ae6b2dcf0e2fe31f3ec7f3e6d5b58a022c1f015a09787111d0717b170b705c2d90d2a4c7f8cf58e32d1acbfc49dc90f60e55714e97c63a1dddfd228a661bc9bedd0a88043e90262e2639b8fad3d50733b0e2f7c9e531030465ac57a4d5a0bbe335174ec1e5346362104a2980d24b3305bf825c1d75be1c1321e0244d9642ff475d3ae45cfb6e1b30da1c2137c7516df11a181bd238f010840dd4c9b59330a88cba288ba78fecc57e24fd0563d408b82dd3d28bb6752d54f18e9feaa44c4cca2ad964a8dfe53a8d9c2fc4570c4b918b87e9ad7dc94cc35030512f85d95bbaeabe7514df1bd3e6e9191368147e2f0ace4d43174ed7ef77f26d31b7ec53eaa52e81620f2b5b0c83cbf79ca8744b9327962d43d2d24d356c9dd7deae2830c10d6eb6d16739c0095005cfb63cf01760ec358ff5ce8f79f3cf1ca6dfdcf3b0d89cff6f9e21b53adca7a4161613b35f9197196838bb50b5b65876f0d417d54df18f49d404cad34bde8222d72b9a6bbc001b405bbc0943e1c310b131e42bf07af8fc54776ce56fd966344c65f480b6d99bafc623a1eb4e5cae545827f6d57c08ab481f02033b78c907474a481839580054c05b744454a8e0fe7b77487f5d9f1bfe625ed2b3ad142f8b306ec9cb842352294abee7f6cf6d5febdead24c6473d3093e504e15b1d2b5f502a4425307056468f130ddccbb2a91bf55445678746fe5ad233e374ee10eec7c3ea3bb86572d73c59b275506e9d8c2af1aeaf79ca0dd67a7f53a7b76c31e9b4d97efb6c4f66b49ff2d7f12b8d02a7f20ae2b4f2d0e30afa960d1c15b622a673dbc57a4f5599d529758635fe845ef8dcd2abbbee4e1e81fde8f675da4c5981934d75e2ade6124b8c584a800a35e40e14bfe1c4ff992cd376e4bfc2002ba35f0050aec7771c5a24edf766b6931af79ad07d3cef4f60ebf4f4c24e0c32b7771304239c2a74ef4481dc9c5deb11617de8426959239b85c2a51812f3c3fa6fc7b10c7e84b41f8e5b5000d4d5196b14c995706b5290eb61055239005519552956a99410470b270adeb02c457253edaa27585a4e1bc01bf45ff3a41c06a60b1a5388516cd03083a4bd5cdb4ac81f1e616562051384017f9996f21a18ed501d97d79f681de2b31676edbde3174ca443039fb237ed820e435b1c9b5af0bba1236af4bda5f7ec94ff7163eeac1869ba8cbd3495682a485989070409139565554f7b41b40943d48652c29770b8842d8c7de2bc875da006e5e5f428169968c5861d954818bdf5d1af7bce5d7b0afcbf8c2e3b442f66de94d98110dd882470b487d2c9332c0b490b32272b27e2c34af8127974cb8176706b5ab821be67d2b2654c7097564d93172eb6fb71f348c85a683b8adb594a329a477382cad3a1e6127b59c67d08a6197feab5587eb8a9b330c751a16ed366025aeddf3c12d236cdfabfc29ca44ca52c87e4c11cbb18db2a83e67df8f447d19b23df4620314b9ce003862ab0cbb8e94fe1b546ea2f8b0f946a8eeccbb5e5db0cabc605fd4af9ff26ad83d8e187aa186045e3f28b4aee702b96351671b6723169e41fa5c0e4d5cd42ebbba4280adeea05512e87886b4150c7f52aec0672c94f026f97fdda2980e37a626f245cb7b40af9be0f1297f8a7e31d95108c2f5ff68809b18bfe8738bcaab028628a959a1d9d4f0e4d5f1f315ce3ed1af5b39c33807ec940c9c6d8d349f2122bfc7a2fdd36db4bfa92205ba13d89c7226d0101da6d163f4c617d9f0a331aa16253335bc4d798c5d126c5fa12e8f15e9f618aa72d3c05a0204cf8f631d18931ca0a7e925af0f23d9fbbd88241791ca7e77bdcedb76c041d2f12fdbaf1f68f4567904c64293766ff1a6e73c6fe9b5a6695ee36f7109d1a34755fd7eaa445f047d4baab961c0f40da98f3b7957c4e0d98069707d9d5736ce08a2dd8b029546cd7862e50f7359bc18144ad86c07e7872490d17ba687bae490dd40b3f332494c930a2fb078702db73bc8edcbb91fcf5e0426122ec0a277f77d3f056259620d63405b023439717dff8093052e744b9e61559cce4f2b6f0bc2b6daf7e02389c8c8d3d90eb2c8ce2da6dfc0738e2373e41870017be700c144e8a0c15babc180c9c9b6ac88a007967d973e21e076a2f237deed024defdd619cbfaae6997eb4b1fdcf7d422c50e408aa4a85edaac4c8a125ae8e7a8b44c353b0bf4fca18ab9d5265cda6dac0241add7bab877c7233b58929d2a5218dae5b22f62886996e0ee49f06c6c2fd2438a1236a2cc4b24949f6a68e21a66e4ade7a1a91d52639452100c5604de79938c4502d8713fce82e90a073af8c96d674578a1d541866bbcafe870da61354447822b3e96e958ae1d7dd2f877d2856eaea030c67718063e65203be2745d1bba429c245e85ac7b2e9f1cb5d5a6fab690cc1f0535241cc7bc76ee010c7971c19e88da05536c831806950a5d10ed6456dd75b6eb838bd5e985ea166a7347b1fec2e490448d757e0822194f774262f07ff2dc48cd028356d30de6be43db61ad2e2235508cfc6926dd1088636599abe0b3aa9efccbf59bddb9af39b7f7f3d7199a816f9a52bed6cdc1dbce9a951ca3f837d48d35c4f43068166d522c79157782e02f9e70fe90e79112d789da33b61767e041d2d2960c499d43f4a908b12badc326fb398d2f9d8146d891d9b11bbdb84a46eb5bcd5ef0817551fae5e0cb9f512c48702a227b7d4d6ea220094c72b7522aa570d568619cc3cd43d27730ab62f2f3dab99c00ef5d5de01267f605b3ba9b3f9f850e3e79c012762cf243d3e6b62bd7c7845037e0ab7d1e03347f737ad62335b85c478c3d6844e1e130cdbe9ee7487d5c24a27b95017d1aa2a1670f70595ff7f34c09ab6b54b051c3879d68ea8c6eba407f4542882e2115482aaae000a75e2b55655851de40c8f2d47c8b9111d6d8173ccc77f850ce17a821303d49a3ccf6d3632c1bb36a78520cc5b4048ebc171e88ee8e7e127377ef1102b04d42686f92ab23e3a3ca47107b264d3b180802b8e255bd10f2e209116668c58abfaa5c9fc2f79d588ebfeb325ff8d1e9045aa0d519b8316bd20a0f7cdd2c7d74c772e8470f3bd151997f7c8de358671ad44521534f1905e4461946d981c6bec0b50010a51eb83f4550aa1fdea6c396e8fb032e51f105d7e26d1054e40883383de1219261e06b059dc2ad59117ac550964d075cdf042461719a3db4e5f59ee8dddfc2c64bb45967eb648f1a367aafafde90216448f190043a9c652d484e448cf4cb343cc6f7727be93ef7656dd6f3e95c54a15a6dca2a90215a32496cbdb28179b11d8dfbb7f30136af19d6d5088cdd32a0ef5cc0bcc1a236ce6f876a5f053a856ab4a20a7f659f380e6c18b81d6995e328eac39a93601be7c55d5e536d8768543a5c796c5322bd37e13cfcd47caa962333bbbb89e0275f2a1a3373454647cc42afad8784776134b736920e1be6dcb90781317e73b56fd9e7c5cc6f39f8558d2423df82a7fdd1d3b026fbabaadb3583a39257160c295f5451aa5a06eb374693204dd0390c60d7ac3f3acd9f68d0ac7edcecb61635e50e5ffce64af52454d5792574e57e90348f590a42745921c9db4a481caad6cfda4c608cd9a1c0cf19d58ae8143ececfb18d3a684b8f0d477f66e63819a920b8044ac4df038485c3fbf6f79f209a03bf638648fc9c7c0d13fd6ad61d040627c63bcf29d88c2902aab537fcc98e399df1690bce82b56b6249fe85429a3222ca7ca589af04416a0523f8dfd42090685743e9755aeae8c8d29285b834d5086f45416ef26edfc46eb5bdf5f0ab3d66845292ef4bd301c922d3add8dfe4879f59628baaca0028347018d0085dd527323c9fbb517b516d825c99e9d10238be142f5b7c07746357a3acfeba60a2d95292b5dfc5fb39d40ea4d711fd15ba28d0cdfb6ca3c6210586550cf7a575d5bf1043184ca9d2faa670216d3cbbced4bb11585de3dd06c6f9edf4ad91d71a8f9ea99956ed67b264d175f86ed4fdd65b7cad5e399f4196be555c30bbf91877c5d2e7be67752ca8acabc4fad1ee78b5b405a65e48f0980e98933983ca6c9f2bd195189ca8740392495ef378732a16b5c80e2e94e85424efcb2cc40105c4b552af973a87ddba97bdbf186f4386fed1875c77bf9f6ec4e10a0e32cdebfea769890ab50b592e85a45c8f0681b34a6a8cc74f8def6c9488dac8af07916301d9319058efec77d299d172be35578b98377f27cd4fc85be6d4fb73496694df7e1c5cef5a1154971d3c4738bdbf7f130f3186130052f22b1b6346aaf3d491e0cd38531e89903eb4e7cc0137ff4deafb49ef126928805ce72d7be88f934d6222a2ab88d6e003ee4a469c1fd926635b91a72c0286e600e89efe20b9c991f29e9e68f6dfef4f91a4884a853dac4fd633137a5887fdf9a2a12e372a660109ee23ddbb7ecf4488089dee72531c59601cd8a6d942d5e60d8609f8c800bb8f73b062b9eec36b85d391acb89669a8ddb0b90069463b5e306dd0d6341ffa1393727295f4d6561a6df18cfff859213f6fc3d127f7c45310a52008d0e883106f362b2d1dc378a0cd69138423eef6d95115110492a96b6dc8a4c6f954bb5eaf80ab46ad5567847e1c441b655e232f40585f22c987f8842e36dfdc723099fe0ba0ae746d7c96f05ba78aeb69220cf2c712c3474569e0de4e4f83179ea0ad0d93408996acc019b35684394de933fb6c20ff564add214707f67bb1f5ac70149a8bc86c0758ef4e72185f146088b47673502fa40c91c06e3cb5443ea06eecc2b58aebd189af75b2ae0e053a99e2317ef0228fd97a6d89004b80aae4fd7077a1eec192e91e053db843390c7862e5a96bfe741d8332dd49057408e8497c7e159d357661f5debeda6ee4f65da9811931dcc3befb994d06449eace50a6c5b5620f92cbc37d5c64b588bb73023d81f695472180637c746c7e58961b79874d0832c3ac83b5e331b482222b50b0dd2432697c044fc87890f0a44255f88d7678aa68ac039f77cc53e7c8c5d93c63dc017ef7cb67fc2849a8a2653e417c8ea2788b1a723e4164dec23fc990b88e79d4c4d8dda39b83560adc71a641653923b0da29d296cad0b0cadaade36042ddeed66ddbb0e996931bb6e90679c6b2acb324107990985a1a64dfe8855e40c3e5578acb387e6e058c2ea09832d22e572a62229686973157d43fbafd3ef9c03d0212792325e6855cc515c2f77d14beaf1632d41109a7456a3689319fc61f99143d4c83dea74f8dd644cb8fa830a72ef550d7c57ff7d1268fb149942ea8b29d8d387c6ebb958117b2b510499993c663d3f4ed80b1ab0a7ac55212f1c0e52f1d18e941ea9a30e68eab6eefd1b7eb2c3e20bf8749c07adb3137d74478aecd6a24b58e196b5835483ecc44bff5f2163a34e30dcd97b7e7c512fd2f7e952be542e45dc58325b1a405b6df6399e8ac903f7a93017034a4374b4a5e50ebd5083f70ba9853948a00f8b43c00fc2270a69505a4abd6141f1b692c7d61735568711ea0e5737c77f3f29022a0b9a3f8dcb7688c8d45982b9f2b8ce31f55be364f945982e61edc528f8858dda75cc82bb22806b4581df782977ae4758c381e6c35f2ffeece636ce84a85f7942e92f21e4e349e33ee664a439cf85d830c9f669e7eaf3333182c32247ac6296bd766ede2e483b66cc337d7115fbd67df0885c364c30fb3bda0f3489dad1dbfc0cff9c6aa3826eb8acaa674288ad649a2337a01f54096b4dc400d381f42d616a3a8431ffeefb9656bd958970b5d7d696a7dd38b28aecd2f167c47ad2728c9885a016968ba396e13a9193e19f31928698e3d66b89ed1c14c018e29ee9f6b67f508e93199394096047d40d5cff00c2047079cb4ac9105ce9ce931004e92930c8f5992447c749d8c188300d5ca14a8a54ee57ccb9e101478ac3db32b2314120b77aa06ac73854335f7211fd2ce3e5f2d93bbeab29b89e4900ad31ad449b7e4d2cff6875474524580f6ffa9be447dc963687fd11bb9a9b08e3ea3227bfc1ad911f0be89181c5396b1e02381234bb0253ec70ef00febdb7ad02a2271cf6ebeb4c0047a9b376ad19a78c2b310e982bd6973ce2d1f131be4f86c417bfdd7c34f970e65638d31ee6ddd326067de5b345d8097bc18ced49ca3c9782f991afab473dc737aa4f573f93a6dd604a192cfdc7206fef6dd4e4cc5f060954770b7134f8eefe1d7ce516f9a0260de2bd9bf33dd1429b4c5592695a82ec8f8ec4725fbae99a95b5e9a290148c99f55c32aebd6474b52fc4d56cd53b1438e146458b913bb17538c67ba2ac4485904f63af6f692e60afccac699380538fdba82814a48e7e66e96cae486e743535ab039c58a4d08377f68aa3614acb4d54b022bcb24de79e2963966bde3e611c632d8a617692cc48f03731afbd0eee3e1a5c1ed5c164514d327f88112f033ff2d6228dae32943565b1a668dd3bfd4f3e598a6aad9fe2d52a5711492c28d70ee80c3c5bb72eecfac16c01566c228eb2ca7d49ef9814f7ee6ed4a604289e5fb3ca571261f83f2fe504fa56fb8912bec1b171f05195f3aae5a9c39b6e671bf4a1aba1fe79d8b792218d9375da5396b5c12292d27bcba68b9513b7ad8adf17236575817cc55e270254f54752d5da195b852f9be6dea82b13907c1370c7095de934c07fd744766a9a0c0a8131f3721eec6688f5a77e1a292af2f9d7f5117732805cdf1e6282a1cc1f08404a0e76fa681556a53cfcb4b7d73630a18ec86829b5f22372184e1e48234b9e4e6d41371039331754030f5c56117d8b483eac0c7a9b14013d360167a0d2474b0d986c4c7ce5c699025ba5b0737af541d68c5fa22b24a4f67464f96c262a1a870c4872bcd5f230150cb9ebf56267c665ab524dc6743ce371f66edbbb6b02fd2a58da63ece59005177622354c843f96a03e90df44e2d449a7ab5feb685bf7a438e639f14f0cabf182402b78002b683c079b3a9bfc230767509d98f6eb43507a5d89cd5dbeb64a0a519abe3b77112a11e8f14eb456b6ebe2ffa1e402dbd842e5a170e8f3d2b0dc282bcb2753c98152175649a88586cf40b609724dd38380c4334bdcf20a1434a37a49f6758aab5f2cbd25b89bb0f22d447fe313f59d7f087c0b1b89f0f591eeadcad4b3bd67f5a7891bdd3817267c3b3d19edb5e8177a339b359c00458dc5e38c2bd2d3ac4947ed61d8bb8d7db3d887053ad12823a44cba2a409b48cbb5657e5c69a35bda0b461412a5782f18a2ae33ea145e4d7aa53b7ba4921efab8263256b7673f529d67920aa7deef32898f34e604292329d153181c7382d7e08d62fddeceb1d703d031439bb87c7c2a0d96cee26bcdde9c8690a184fbb03a39df3399ca2ae5a586ff49553e6db2dd8d4d0557673c138f872920a2e82c63d140f2517a1e8eea2f48ca1ec19120cf1510901e463669dbc564872b79c95a21bf1b774bc77983834398a574fbbe3f038a0a5373fb35765011cd539c8ba54ca49fb3e96ef7f5f042cc1f697f587dc922c7bc489511940999663193ab047f0d3d66e746d77ae78677b9829d4e4edfcf4063254c8f1c0339e0732f26673f77e6973809de86b15ca3f4dede58144adaa848c4e8dc84f4da034d21421b92e2cbffbb7839a240ac946abfddaa69b4912e787b8200852bf02f5b9452d12c617e613fb5aa0c46e8e18ecd89314b2a1ce45c1a815bb11f0f747eaaf0aa0a87e8fac212b178731e45978405703d000557b463175fd1c175f487a513c82dc4bc4a8446ef1892ccc3bbf3a832c828fbc9ae735b999c7ccce5a9ee972cc2d81bd5ad5a578319a7754293fdfcbb571d68a8eb69898da4e327ee93d90a1d9731c7b60743d9c4de34958ec1be25406ff790a276c6e13d2931ef0efca86fc82bf17f645e5e91cbea76b1d47ad1e876e4486a9bb7d9d231941366f1bcada308eafef6b6961722c2545f94ab896115a3c41119d69c43f176e7ceb9d96c8531534343e771d551b7da35edc35542f46e5ad0bf9867d151ab5a825ebef4fa44dc818e977e9cafd740f492c9e94afa131cd89f444dc078ca3f90b291969e298a8e0299f1342b502b953d82723f10c105c9793b2c5a6903aff11371b6ca3f781a28948212bfcad728ac738725260ab53c5f9e51d76986f187cf9122f75f0fed7c258d9164f15c0c164d1c0a6a87f3b77612103fc0c7cbfae7fc74397cdd1bc1efbd5cda9075e8318ba9464e6f20723cb6a95744fd6497101bc9ebe5a1a99c7311708293104c8cb46ab8901c81369063f5769604f54c4b1c948233b898adf06598c57e6036adffb65cb1f0c21dbdae4fd442e91b496bcc0b07a345bcf9a8c6039a1167092c5997aaa991535a8b4f41655e11c5823a7594510c2edac3c0e12933bf55277c8437dfa50287c611f1be64e850a01b93b8edf095fe2466c75fb8a50954e619607a64569fed85211881f2dd935680e76f8f59f1047ec964ff5a261487b12d6e54bd643ec803093c70ac4406854d8487e00c1feabc77c3d74f6b0bacbd326e780fb4abb76067ecd9a84c88b18cf95c10a87bf1e073884dc8f1a46d99906f82e985f554a0659ba4842ab85625f653bbf69385f4fd2df19c57ec9877431d7ba97243f002fef257f999af7a1decceda384d2153f1c872511a316ecefb273aca5345bcd34443e750689ac922dab2c971d5e97108b1597f9729085292477d74c834b8dfe80811d4044c236713950ca24b7092a93c0f951911fc6ea0b3160f9541a7316974cfb58c961ecb5f7b788a467169314fe6f30c640ac81f69e9ffce67574bc3ae17f5fcda0bc70d73dea8408936e380eb0e84d6f9917a8c67f4863f1285443383a8e38fc8f2fd2816000d66d9e57c8441982f72f698d70c0517912907a02e5e877eafb872fa16073111b9383e1123e79ca7e998ee3d23ca9fc4d9fa39f2b40ba27520053926143c0a34292e472fe7d99c9a5ae7cfe01c2e375fd28e5199bb6aebd34a11d846a12c2d3c72eaad30565cac83c4c6d053122bd24f23d5f359e7499687a3690a1a46af160e7b085bf40678c85df51de46f978a0643da9a572fd5f3f662905fc623ef414b484269c0c46e8664b463c22490e6c9c4777a27fc2509446521306461b9a502e326d35c79bfcf863ff5de1f27bed5584beec816502a7b499c2a12574888b17fb43224dd71baf8d0a0be67bb7b40693d81881f0d703ab3bdc8d392fb3c8138c9506d471e65b1a0565d8f655b116923487170ff90f3fc918f60bf3e00c3fd645f232d0dda68cabffe2751d7c0a59d981b1f967b7e204f5d6f2fc5775842bae5532d566a31e7b8ff722678ceac681fa957a6d8e1f2b10784de39684049ff97415b880c44fa8e7ee797961b64f9da4b491724a127430dd6245059c6db2135f8840978dd86eea56c70c0ca1fa90f82faca6f7d58749366e644f93789171811f0c99306fb64c9b9db11edeb1f206d44de7c4c35fed8422e7a6063f39937203dd9c2670b0b1179da3caf4d7ce105853905931b55b134cdf2f78728a84c3c61feef707fbcbf10d0794edf169d330e5d6d3148cd30db7c24a4aa9e26c8ad769701e568f9c4565733e1fb15d0815faef0b6f893bcef1016bf71a2dfedeea68e016018b82541268b9c20b9b6cb643a7e3ca34fcaac5ea3e73680e1bddbc83c4a12ddfe58b3bb65ece5c430d2c3286f0f11100de1fa599b6da2330c89cb68673cdbf9d181cc8af92b6d0d136cacdd187325d8198a2152b50067f9fe5c8c9f8c022ede1f057fa4b42c0e40a0ce03af0c918dfce7743f89df2ae16df059f9b7179f20d98c54ed290a2bfe0020276186484d07228344c732097dfd19f4a24cd37af4d16852231ae01b98d16e4e5568dfdaf6dffb2787e32d0316ea4979ca4d08153a2785da0c538cd74270f62463060b7f3f3f14e12e06245b129efcd4ebcc49d3d7f7f93eb2a570b834564833250ad9b83cf6942f93f5a715f819702bf496a429d18733b72baebb34062a99a99d9d5c59270fb4d47927fec7e53b09abedc0429d1b3903656b019b82c2fa8c9dedeb903cf0a9c871afca2ba0db600c6dff94ffd5ba9991ce88fa6a4173031d064487d2e10b5eb4b4f9765784a7430ea0e6af273707bb3b2570594efd52960da10a5745c4f0f3c278a044f58b1af2b896e785ed6b1903d5e0b88b796b0bc8efb614f05fab7c6e40c3585ef5244471715a44563eb76de5f3ac60770a1b6ed17277cfbce5f14b9d3042cece0f59b736600e4d4eb2b1768b03afbc28207bc0bec2e9397b8d9013031f6388ed4c2bd3468695b82d163454b4f378b598a890554ef4976f426f347dcf8217fbb7bc107ca27d76b5ab06f1d58b0bb29b619ce52e299c56a0a5ad6a2b1f7a9d9d2eea8aa443aed2041cc1cfc0b6c8330d11ec53036dd3290b37266f92267e1fb268c3e12c2eecd19042fb9a15f85277b9a36567ec138a085a0b27c8b63f168b7b62858e8bbf713dabd58d75d07983043392084133cba31143e37a701ee44d83af4333309ef9c5d2c66196a2af4048d9f34f726b22565da85d0286b6c2178ec14eee9c06f96182da3575850d8451cde76d776ee88fe3103174baff81bc0257e8a637f91c4230301c306f84e3bfb3895e2878c25047a753269e0044d7b91d585b5eb8dd2ffbb0a7a9dd8f3743351e73b4c9349eecdb6204050ebbc1d204a49710a57a821943189384d7eae2c5d6c34fc6b968be24d11256b708d67c317abc289f420077f998ce03acc715e32981275184c13b96464fa38b8d825e03f093a0c07ec8e70ea4291fe13a291d52dbe7000d13716995720da841c3e7867c5817b26d57f07d4db2ff05591b5b31d4bb0081366df96889c56a1c5c5ff015a78a01249f313a8079c6c5e80059b881544c4359cad6d55aad29face494d2c0426d8b86ed1822bda29a82e70afe32bb229d70e4a387b3d26f1f11769f0e3d99f8b7eb20865fabab63308fc5a0c8d662eeb4db764328cba822506546a9e701cdadf9b3324537f6a9b6ee34b1ddf85ca8657d11e3ea747f81e069e20dcc7444b0da67bbf64d754ad20e4865506d555cf0692a99c33cdff2de4a055f4b4bc5e014a78ca2e2c8851ee0996a9d02caef3f251cf6367729a1901f8f677aa2ddb68ecfe937caf7167b3a8853b2f0d4b6e5f4f3a9b09638d82ef276c2144fbef5aad0d24876216fd0e1b15aa7a7163b1d1f1fc67917af307c907ee27d56ef47423fbdd1cdcdaa27cfa1f3072a4af84f646a79ddd80abdba5061ee05a818e03423ca07c82a8971545ef4a71515235672cea8161a1540a84dc22e7b896661b4c8b2c08b5b29e6f4f8183ac69f9a485f4b39eae6d5711db4cff1dea23beaf6f345c9d666a2f656944abfed1ff2f8e74e52f315f2518ac8d32ecdec91abe5da7cdc3679a08a10054c430f9b825461d1777f327912c1cad2a55cb0ebce1b2fd57d5e882904f8a8680140f46ff5734891c2e9e8e37415b3000220dfd8a13fab7a08dc44a021365be66ddc3a6a3e9e26b7ced400fb400a455e57a14607c4e2a3dfbcf625585644f98ce563538f9450f7a1beb8b046d58a57581660871cbf3854af8954ebe318192c3c4543483c44cb4b89953a594e85e44320aa60cbd73248d625e8bd5cfe2d4ff2378be2bc0b3e70129a1b45dc9cef96cf037e456ef9c075cd5d9ea97969daa5e2abe80469b48b3d18a7d4464b29c0b89de21df42509bd1e2fd8aa59a4ecbbcc7e48311996fda5d601e04b51da3e61a0ccda1466334006294c9d67273c640f09242af65b3b4fa1cd127daccbd42d4c1e5b9c2baa89ceaa2c2f12e79f82708bae93ba9b81883e5cab44cc1f9fe4ec1b3da7d01338ab8ab0f434aa481437acf43a40b9155e8a60b2869bde27a7817a11d242d8001128fa66524203f326a752f2ada30a68542e6b1f2f02c6da70cf37bad1fe7427b864b3c2529f20cb47067ddaeff03a28f0dd9b952fa14d446d90016b8de745f0d9b6bc76dc1833083092dab915bb9365150a2ba3446520e5975b613341a500e65f2985238f06a34b9f81a392914502c98896c4ec9a2cc2550827fa99753d3d09ed12e73227b170b895ac7b51b55720baedadbde23e1c9d391d9a6a5b964b9d4d138754bc92c91ff697eab3b0ee7e3d59c3bd9f0deef88cd68ef1e66fd9d59eb0258caba83d795cb04405ee6fa8a9e1c64e0bdbe8dda18283e18cc2caed40e923ed02dd3169ae897ce10303abef7b31ed926cbd881c15dc8d8e9e3eb791ab5db4b0ddf27d584fb4ef561eadbcb99f92cf2f761ce47af2f9d25cd6f670931a6dc2ddfeb22d42efc0f754bf92f46804efa2c8fac71696a23ef9b8da22b51712dbc78f8dceb90edb1c4db03d3b2af8a4aea0ea68bc0e4b4ea1db566c50cff657a958e4e7dfbdb527a0224c0d9306610e05b6fe5b059f2f4414b405b9a249d620fb239b77990b3c901328da08608dbc0d90dce27861f006f1c4ad268e368049c4594db89e017ba36a0677befee98d3e1108fdae2dc15d3ae99e8ba4bca61d6aaf697ff153ea6fa781cb3dd39facf7c9796540b6d47060c4f2aed7f5b911ba1aa9b6fd87ce6bc5ed733a87ce3590170e9e9da1d981d498df92cdf3543ef42017c93ed0dbc4680799bb95f41d9b0ba36d4063c91fe367fc9906544c047e1053b861ae4ae8cef390b84f6b67bc2c30bef85ad2b3180798808a65774af01f9c7c1e6cc7fdc4f04f95c84f89d42d83523f72a1d8841bf76672eccb9ea665b9183a875ded0c90f2862936667c02e159b3e6edfea20a9c12ed4f7e4ef1d3bfe50e5eefc061a5ea7700b9d1d9e741e79b0a83c86cb1f9592ad85d59a96bd364db484d0b4d823865de793bdd9062c8062de7d6e1a246590c25e09f1521d2ac1262f36dabb3dff63655cecd3927494efcfc79eacd2c45bc08ddae2db64c78bb8603e70ec9483eac0c3805b18d81491970b3ae83b27b4af6182b321fd78b27d5dc5e0ca918ce9b969399da1ff09585764da48697dbf049ba7a8bf12d4d6650e2bdedf9c0955ffdd36026e4774ca06269065de08629a577fc0327bfe8e834de4f742ca8540199c74f88ac24dfe4c204e4e51d806f06a97c314bb9e67ced777f0a31f6ad41cc78f850be8c71aab2c917b4b8e1945aef5e3b6fcf7788c3920c6d256b271a61eb33fb7173c1fb9406f92d1cd380b3823e5cbdeaa5dc04a10027a456c7f05741c16e75ed8879d1dbd1188bb3ca8d3819b8958452f65ddbd13dce0d8df6938f291f74923d785db8af37d837f54bd4a758773a421ce19b479e7b0b40676ec66281bd0397c6067d8143ef1a5be5e535ca622b843fcb74ab5d249f3eaa6decc229211fbfbd3eb24b2a54f081b698eb5d0c1e66200aa7a18c3fd3b7a10f50cf066df9878164a03c23d40537924cc0648080bd3d13753cb1ba0b5fde150b8e6b441719bfbf1114e86cecabee0bde17e16dcd6b973fc09cbb3fd5bdcfba2ce66f420766fd91f82c1c549aa4d6bb03048b8db78be62b1d31835e62dd286aa23c101f80dcabd36fd431b29d0f4cadb872753b84c184c40661733c2e1ea53a1fb78bda4f9da11c6a6446389dcd7bbbb2652aac1215346b18ea2d25e1734c007fcb99a683572e372691e8f088b8379d200ecefd278404493e8dd1c592a02bb47f0fe404d093857a4aada9fc4707963105da6e2204fa324772ee567e0d724765569368eb3a3df251e846655b58ea8cb0a999a11e8362fe9888834f324880a3781282236039219ce5979becc35cea7561a0f7e8eb1811a94e723cec629c0edd48da5d8bfebe7629ae503a47b59804e89793d5ecf0336a399a3d53d162b6bf7c41821b081c89d188506def14c8fb5cbdab8e910f7b4fd4005f15efe8797efc0ea38613635240f80d7b5ab3dd6f86906675d5d834762f99bb582f5e01713736a5cad645357c4a437de0c2b701c09006026c9c39de2c481a6e3ea4300674ef659eb238aaf20b54e8141d8524f610285b319f22ce2bb5b2a78ec5fce7f260718d31235ec92dcf55353496ec566e67657f6b9d01db0102f3d0f482539bd4f441b554c7283eb3ef0a81a2dce4ecc57bbfa6448c25746e548c4233a553b06cf70425f1f3de6e1d56b1c2c4b9bcc992a4b7a35370768cb67a003a476c96536db8c1dcc34467e8b7864f507e86235658fa386da6568c716eb25c3fee26f13b5411de4fc818ec2efa52c0323daa48679c7148eafddea79247438ba56ef283943a121e5c7af240ca84a761b6e9b6200c3ed4c9bdbd8d2dfe67ab49059da0f1db5b588c2fcf1600551a6450f6143de16e5b1a58b87d03e02b29a67cd25f019ea663a19b6781de259bfb4394d41c9b29e708954351932a1de9113e661f0c5503297360c5fe47298dcf4ba6cf405574a4218f01ea848d7fab17dad1af19aa9096d962e48a70e1c00ca61bd81da99f1aecc6e00ff22cd55dc13354b201f7b26da2f12dbbff846185c6dd4c2eb413286c258cf5617a67ef760498dacbff145d43400a1ddbf9fb066f1ccdf5c407a3b62d6b6980f42057e9b35668fffeda440ba8392518fb1b556ece19af39f5481dedb3a3c05ae53d6b68f8970da1600875fa0853aadbd3ffc8fb834219901454a7ae0418a7b3313050cebd6739248d8450f92fdf0e7f774176c980d9eb1562295eeeb75eb0c4983581564db3c4486614a4ff8d4bb0d64132dfdf4b048412da75d5f072dcc6d34a202c71da85559a4fd7112de5f0cd4de96b8267ccd9827a47d75b6e67f4b512c93673df8b4297ebf3c0b700bec19fe10d143db72bfcef42d67276a1ca77f36fc0fa6a9eccb0c8028aec35110b8cd9ea55d14c159182f1bc771c1e274335663e6bd52db32c908580a10b42ec4416856eb4173904079bfbfc114eb144d266ef2f986d4f1746fe6714c166c29a48860bf5088f684774e4658c79cf01198f14c3e1ca0b32e5b9abed1aabefe8104aec23c18d4d8b941b2a24a0fa7147018ad2b7a2a3a6c987073ae36f216f89b46eaa59b4cd25230a33756036a567cb7f04b08ba001131d3381931942e7ea2039915a1691a5db6efef4ec6dbdb9d536685a0d75fce68d8f4fe22107db33f226296db6043cdf540db6891cf79de9001a57818a33e11c807c2dd1223069ed2e8119d27bb4208d32da6ff97fd2b4c28788ea839f1fed870a936cdbd4a2908f8136037e4491ef721c491465b3004c171ee288f22074e6e313fcab2b213342cac9432aeaadf1c4cf2c04c46fe41bb50fa53ed480ea161ec0fdd10c92ad3fa31aa23bb859d6b9e73c8aa296723485051b6fa78fa17f113893e5f4278f7ce324a5bda881c9bcab53859768e035ce4cdcae6eefd3bbc4eb6dbb1a41f96ac6118ead22ffa2facc0cefbe30d748c3e3cf6aeae543268981ecbdf641dd22bfe9fd50a85f1e6102c6841bf5daf35f8c970f195b45a6c5879fdacc736a93d99560c7f235d421033268ba09fa3b9e96111d2f670a65a1532834fbd879945b6c78063ba937add00727643652ada29037a285aa11bb20f347d84ecbb070c9324a7412f021f76d775ea5cdacd6722a030b998ae452ea2dce06bcb6d84e05b4882e979dfc73c5121bf5b05b91a1f69efde82f9fa4d3bd25274a6ff42213f7dd354c6d98578fbbe62f346090a9bf49b259ceb78b3891f36e5871078bfcf3aef77e71a2edee108688f871726f96cd3f6403ad3e9399fc0460dc85989151f1805da255675772f8991f7bd90e36338efb63f7a7c89bdccead1613cb2d506b9d51ecf4f82688b9941e32304a26cb6fcbf71807c7fff92fbd202a21aac15f912fe051864285857d013cf91fa2134498e433a475511a000ef0fa70ab829ab1f1df9ba2bab18f315a9ed9d916b4deac241295790fcffb93b46ad0cc0bb6dfd23f26a405eea888f4328932bf0471f38019e62a6f2c22bce5b477ddc765598c912e737ba5b35a6bc6d632e133e2163cb2e77da026a8d573a805a00cf7d9fe4ab2dcef73f70edd39d8aa39bcb2af736adf9abe6d7f7803ba8a35a8dfc75d4e87dee14145baae0cc71207d86d5108ef07738afd1824c25e963d1ccfb8f31aed5d68015965468d8afb2dc7266b933728d5f65512f0aa98f352190b46f59e76a03c983571c70d7e561f1a18784305013018abf0250277edbaf5c03c6991812889359480c26a0ff020a3529cefefb1e711c724e3e769dad37f7e2c06475c123f4c137fe52db9a328f4861337f62e375774970391777a0d974419b844df6d90a145c5731aed6b276858a804e6b2eb07c9223c11d42f9b1f0fbd1561c00db2babf8325126df283aff6aaacadc357b4d24f6f86ec716d56e2c981e3e75ffbce016aa9da73d8cea1dd0d9f76b3f866975b7460c88e2ac745ee237f6df01c47470a77db3a5f928b57b0c0d94a28b10b8888535c59d19ac7dd2024ded08d416c54af760ab15cedba610b950832041b99e2cd673e4d53a094fc9be493bc801a6f1054c257414f4affedd01af30d6a21d4ade111bbdd6d5c2ccd3d5e1bd95dda3e47806040ff42d1753d57120cc0f56a175fdd4d5fee1f17323720eefa6c07b0274df5c2181bbc59081cca95ea28b44f034011c766aac9fe8bd051011e7af0995386081965c40aff186629830c1dcd74b1bb3ab772fb674c071203e2ccee2446eeb2c5f1da8d546bf92cae43f3c34c162439119e663ac81d33e46f11d989d35adbbde0f3d8cf0f6ad7706792f97e759f933c7b9be7ff3e88bad1ee205671cce3263cde876419ed56be11bbe1be550cc0323dabac373cb0c90652e6136485ec551c4c6b6cfefd6c53c0326b3967f7d2a26c83086febe64835e1045c0e86896270b88a45465ef2e34bd74f34afef39f1e21623a3a2dbf4a611d8b076e81fd9e733ade327e993963459ff1a3d2c32aec41458f56577c56fca2faface36f9d6746c3c954692e88e5a7332043a0485e74559ddda982e5ac4789c2ba874a158fc40485e6dd9a7a3e6690d9749423cff6d96f96e7fe827e320875bf4b2e38ac82666fed5fd6db2a54d5449a0988fecd0a95df308902f9f7da28315ab0f2fe699c491eaa934f40485e5fceeab4f0fed937f5b1181fe295c3cd9b7e99de0e10abc250630744f5e51a3e34a2014cd2a7132c8be8f728515e0389e2296c88aeb336b5eb756cb7c39c7888435583da005ad07b0f354a233f9197d7100f62b014e68a74e2cb93722ecf45bb9dd402ff76050ff548c37ba782bd6b5f9700d5202916e43bef33319d2bdc50bcf5c00d4883a6a13d0d255435d7339388ef2703bfa5d3d26b7268700873030913f1d88b03b94b21906601b542303a772a5ffae3122ffa27279fe2d636b6ca7d24f6d2d1c7770a615d2d23499bfa488ed627d4ad05d71417224dfaefa7f4330bbaa05e103d15a89717a728e713a6d4e76a08e1643f61be20f3b72a238719a50d5c553f713bcb9093d721d2389407cc93db212a6af6744da45fc5c8743fa7e313810f5a9366dd6b26987ccc8b578c4473baecbc1d219a2a416497a9c50b46a1adb5376adccb4dc4ad8e048925f92f54cb9c4aeae12415bee22284cbfa46371c04383ea6c2e9182f2df41e366cafaa6f2f5200ae689e4d4c98ff128bbcc5554dc9f8542d221d2fb1a8fbeafae6a30a36dcaf7a403a7d8f81ccd6032e1b2626f68a6482e4f05f3796581b23189231718c5a3ca4ecea2a1be0fe37a774f928267707e2cfa6b7ca34c2aa632d71551a6db3a126fd6634479cda65f7c768b53b7e454c4a20cbcba7cb727b10739c593f0e8d56c715501553a102692392ab3b270c39a4a057c78723b4e6edd497fd09f6c5954554fda5332771748fa35dc58dfa0b5b11510603f26f6cd7c8229834e52d0307b871b7edc34e533d20ae888c407009b8dcbf9a8b94fc3c066c48306bee68a766e0fbcc22c285362f29c5b6ce56bfdd8039158b8a4a76ebe58fd18bbe201b2cd206934d839d727334b1a83065d76c5ac568bc22329609b199768edfb9add8a82a63810ae8781b13dabb630f124880a12a64fe6a28ec5d8bf4a2da398b895c74969c2822eb96abb078b816e80cb3e8f2d950575c6b33e5b8f5799bf4f4deb011d82105b04d673b2acb042df4c8bb71f72d9c414593306ec4e2511bc130080d40feeb9c211f99a45d9bc2e9eb474f8cb99174167e51a8fe268ea4c28aa87d9b9000ee38294ae1fb98c9dc0dcd7a772539e66cb6b8431bcead9c3c7c2d5fe9c4e3eeeffb1c169b6c536ea1761fb08dff62c9b59c13fc8c1ff3aafb2082a584a6a6342cc1eae1b84f044c94516bc3fb7acc90503155dbfb5c0a3451c0312aca21f5f59f25a4dd567bd0873726a53281f8dafd2248e492b2d729b14d0db9cb1a74e5c1d651c26b53903d4919fc24e7eae704bc2dfd49357585a183677fdd0ee5ea8e0d820f2f3ad9514920c2e6ffee0f57a0972c04643a2ed75abf86db1483bf01400bbfcf7ac88140d135f9ab353e91ef4fa37924464ea6391f3cfcbe3e7a658bb64ecb799be93954e5ebfa7163e992e366637a9ed4aa12fd825063b0d8d2c51d5f53bd895eaf246ff0e8ea3344a7992bbd724a11d3cd4e957b78d31bfc19ec19d9dcef08f0e9027cc9e6bb48a6334d2c02b5a70e3034024dd1b39afaf060bc07a67c614077b0fb1cda9194c4675b08ffb01e51947f1ec7134f8de4cbf1ee47ac83700e83d67a7edb0ccc21f174e7d6eede4e1b7c38d43d461659bbd5b02c644192cb60acd634a9f1329df1e72f537eeb53ca61ebdcfba8ab7684e500d08f5520db10a3f4088ea51d233964312c167cbb41aac8e17c78dcec52eee01c9df8fae22bb64db90b2bee1afac286437e45569ed597bf52786bcebcd49bb1a14486209b64b36fa1c5b5b096749fd706d3aa21e8fb7b90b113cee02466ac50ae6b7ee4361a5d792b9bad06e1ad679c4ba7a2e4312b62e6baa8554aec9369e861e67e4f08b12c51d1516ddb2187febda8191e5f51c7120a18627a6eb1758074e03c588eb8315ab7e53be49c618e49ab3cfd36b3e1932dc851b9ee38c738221014eac0029a79bb5375eb04604e578ddef8c9354edfb5f6269d9bb27f37954bf16fd10166f6a4b1089ab1268658367b1838b1156a5394788099f3205a6c7c866ae9b5709f8c3b9744ee111b4f63f68729da8ba26a0da8b9d77afd93de787132309bcec3c66bafb5307ccf0b4a82697772ca3d3295588cf17aec84e89e1c6ff9681f5477fec3282a1590783677c21c2bd2f5038b96393e0c97cfddcc10ffc442f968d679f0e7792cd629e028d76330ac69edf8a4f90bb005fe0ab3dde037fbaefff626a98c43361ed6aeba7fa5ee919a755813cdf3675e557764bf50cc5afff1c03d1ee84df22c66586582759b981c1b94f28a5f2cabea86306ac74cd8721b9a21220a49accc8d699411cbef9c1d52473555a02b508a0a7f55b5a4f44a5dad4eddc3ee7d9c15fc1af5603a455fadefda71cbb4616da67cd2dfca7efe5ed360c67337bb43b00ef6ffad075dc7c289a558b39ddff50983b3d5370549b0b2f8f3421067757839a17fda98075f03754bbf2800747e12da4114d25dd85b1d65abddac2f128b2bfb15537d907e20e3c5ae1935f650dc687e9a3447d5ed93f15b1d30897dadabf66a3665ce0def02c49c45eff571a26321b8fd28a43ff2ef5e083c7ac8895dd053da44ac04da9b19c05b6891adae2c66b2852a3f078c0392ee23e70ea2312dff59ddf5f154d92afb838c5271bdaca466a6bb27f12637870081803b919c4f2902248268a0a6dd3d4d65b12575252b854367af76963ef57bc82216460db73c05c4c206694d60a68779e729cdb1747c42315f0ac400be0e20022828f098ac6bd5e58b80ea0f229d05c5f7505d7a8427e9cafb405d82de671b412baad7d7a72d4ab2d531d74d4f2e8eee0dcbdfc2f76876a3e2238b1574daa74c37b1d41ed06fc81c1f99d4a03cfa462a2f4a20d0d5a64cc8510414d3fbcc1a36841a7b99786cadc708501ac7adca5dad77a7c6909c816740ea060b63450c0d02104412d18fe9015adbfea937cc2f9fb446811af0de070b1a9c457740050e66814bee1f78586940ddc3d16283e5816587d66e5438fb971e07a6ed0c686a61459484aed6fff6914f5ac87d405d4c0b8b6f1c8fbcd48e994353bf1573d8b6d216a072815cf29e341290464c991e147286e8b90b3fa9124e07869b2ac1cfc665184e896ca0f99014fd38e3a080efcad4646aff8c6cbcdb9936d71ba0ddbda9537ea94f4dcb38101a02705e8d5684834b082b38443414c58a1ed646ee48ecf18d720a8d835bc9b453a26d91c21fdc4d022abcba60d599e3e831d2b8b046071d71cbd9db03d1368dd8e978009e58f8eda688fbc1037a517d2bf664a0551e5b0bbbbc89b1d6b5bc6d813076936ec20586da9e1bed69ff9d58be392ff1fbb89651b4ced25c4c42f7c4b8d276b76d95f8672da07429218b7fd904163232606789e71e8795cb75426407d43f6048015d53fa1ca5f99cb39978a454eb123b5cb98e5bf6f259217c0f41efd60e207613506f057fde216203910b8f7a1ac37ce35b0962a13ca583d8d993ea3c3c7f74f46b0fff25b0ca1bb16261cb5f577444f4564ad0b18d8ae78c7bc828506a3678509b122509c3ffc941590aaaa9fdf6954e1af18f007aafc6e80567df61ec44a1f2141f9a25029d7506dbc63a0f4d04aff99d47b82d98c595aa6a35f4d27ac002f1464cfd80e47873eb2e11d05ad8b95f9276fb0ddc4cd4d2de2e78cd8d8bbbe50046667707081e69cb505514948189b71da2c5e20ea4c2ed97d336460fa486b3a57940622429de3605339066370014b3d9bd68c16358f2c9d2c50fd55d03f00b714fb997b9991966f829816f3dacb36bb006ba584363e02c4e1528fef8515e9b4ae61feac5eb8a813b2eece1d89e45bed6d93e7b7a997bec804e9e5db0d144667f62e888a64fb198c302c749a2a9643654f74bcfb020698d80ad0a24fc5703712c4be59b0de04e06e387662426686026883aa3a9ed4d641f3f1ba44b9e56a9d45c402dadec530d61798de632a695bacb3a807d1177c2914b60d1e0c15f88cb4b7b86f35aa459d319265e6f31a02dcaf8dd84cf2b1a79cedce54da8ef06562e8c0e51676540a6e4f54dd9b60b7a8b02ea0d636177b038f135b05f6ffb14949882db6db4757990c00329a7e22b2e77320e6e4d9252686989667d9d84b98a3e52655f701d63e2a248002f09d8ee35f842fdf94b60529d92737e459c4e9df6926ba6fa34904d64332db982e7f6ddca0e700df8a0bbe63273b85a9b080ab1748a72b0e952cd2ff4061ad8f1573ab5d24ff5d1448f6183f8a2d53503d6754e0781815c6127fe59306d006a334ef909071f5c8f0a659248ab1cf0f8307f50e86801494dfd58e2694d2d6306820078151d7ce690c332e34e92a435d815fef02f716ee6a2483b3b2d583385d778143dc8e97b82e795bb714aa5a89350c28d36a16cc6fa33cd091421a770291cb1b7dd4108c0ea8420b122f7ecb05d0df56620773a2bd645b605d2d75dd4138e72c4e421967c74a637bc1cf44f20936360ffbff875c1e0136705b66484f2dd9225b1aea64ee281828d757cc694ba835ab2a8c3c0a65c075cb720602ac6f214a5054c6a28532b5680483a2437ed9af9fc5aaff471ae4673a8f897a495aa42e66a4d2114e2a85f555b58e31737bc8c1cfec347050d1410d0f6db57ba2c11035d7e21c5d80e9e56c70326b3c69ad12c350c96da476936809e0c283d3b849788351ba37d54e924928952328d3bb1acaf75ab09fc860f4ddbd3e61479a72bd3bcf4de6b50f7e725e04875bb65963b095485d59ff24379049c90186f34084b2ce8b72a6713f12459952a5018a79b4c774a305d5d704bfd8597a510febdbcc3bca5602b79ad0e994b081509ced51b36502bbd700c1c119f2baace22d8a659056f964dad6ff9e5fef6db9a9d596c654d387217afecafb1f18025292ea1e46a67190a200d0d718b5023bdd96732ebbd9899c960899c8300e81a1bca54560b8a35522b9c297ab4799c0bb26237ea3def804938cd4523a3a0d92158d1bafa1a7157bfae60c9bf14a6402ab213b1bc7fd15bc7b53282a592867ab75d4653e2c8fad54d037df0f5e826195925a555e19e0f576717d07d84b784a72cb1bd9ff48386e79a660752d51e19851e0604fdeb1d532ccd3da70d56b5273d48d37549ce6237f7d251ad31b511c3ba94b8a46396d1ad66ac64b33ed3d771ddca87feb824b4aaf8d83a61f2fe0d42df7ab8e16334ff8e3eed2a0a9f402ae607f625597e02c2e23872416c9fea23f564d3375c1adc4b458ca3c322150c471e0fb629d0fcb10d09c4472a770d8722f80e6173d3aadacebbf5c3ae55ba1de313974ca0a50b841495d5345cfc9e918af59382686126c7e42776bc3775e1ca9695cc6b08e0fccd9ed903a9454f2605738280fedd287c1d92d64c2302fad4dbb335ea6a0879f550500e6a378edb31c8a64d3c8f9adf201e7381aa50f71d02d258502b191b81cade0b7a517350d2bf8a93930660ab3e6e8e6658786c6fd6febe9cf9ae0c5deace32ce803a09f0869631a84a288685d9e233ef14b7d0e156e86aad2e6bdcba6b45ea95de2a303d529c2da1861df676240a478c74728ffa1ee181922e5faaa713ff0836e281c3f70b68a88d0b6da1822a36e0e84b423571b986a06b4e43b129f5be85ca790477e84ba4e6f2d701a77db07c653d4da0597d9e193fcf5342dbbdd727391043e88651fd9899239d0260d731fd2aa0f5543e5085dd16416573d5c1fdaa0f92a94b4839da32eb3e7db6b6a3742466558ed860805bea94e29beb9749b99702cdd38e80caa1a0a2c1f5ea32f65568efaaaf694346dc80843e1743c1a2a32474d078139670bc17e2f2715b22aa759e1f71b1850ef1ffe48507d90a35749cbb6649807e487f24dba9ef03fc96f2b6c6714d33d5746d2004365c21e841126b7764c5107898c1e4e77dd3c9e586fc6d52d56c37dbe46830670c0edb7bef007835888d8854b8394499f305625a7b19fc07a50f529a080e076154a30745e95715d22fe67f1bdc4ca647da843eb03a74829ca493581f7c96084ef7bd2a441de821f28fae92210b5222447a6ce570be1ffcce30e4e4f96a77e41fc00bb9c18991e3e6f517437b306fba5acfc24c141a8117f587935822c8635bc3e3f7e66d5024277878cab76073fca5cc8d9336cfbbcb0ae048a71ddf099575beb190bcec11c59b44ffc82ccb650351d40e9d0af6ec16d7b0d5b1ddd67115e194366c8e34dbb10449ec97322ad18d78c11720d403356b366288fb11117321e8c4616cdafe938c3214548dbfa195db57817318248f6ed9ba679b993a9aa870875b0441f0eae7883272c27f6186fa9202f9e20589e78365ee84f5ca045016df0ceb1977ac3c34495b960ae9044cf822f6c892605b2347d87fbcc133f586d54d8fae6d43b1cb05cd1ab88d0a7e1257e5748336825232a15130c4037a13535072f7218613e2731eb2e47afd2f84002b15efc6ed2c9ead1537b7392a7ca84a7de86ee8a08d15422ec20619b008f4f3ad92a7eda57155a797aaa5dfa10d76094e26fc0d72711f5bb302ca78ed652c45290bedefde5d3a299d14731c319c441be3cf5481a14414172a740c99978bead09cbb37f0210bd19c44845d85040c192311efa0a523faa068ef36aecb174b2c95a7b8f4b41be7b5d9e60995d97042733a5025a7867bd2fca1176eaa8914625b1e1057e15a035c959722871ea9fd32880338d9aef52ece024fd3c271fe4a10517e3c15f5ec2816a18e15ac5bfdd20217eb8844e00d9660e96e0088965957372ea3b2f2f3ea62dbaa24a11da2941626855becdad7eca01fbeb6cda1a3ab974334fe9a1ae3398331ddc41c882d1d5c2358429d06695aa9572969d567595d69fc71d3a44df23e51ac03f2beba7406deaeb0a08ece98afcc528173184f508da231bbbb1f1ec72f294cf3f41e35f357ceeb1b199fbaf2078bb710b6c5d7cf82e359339e268135300a91a302204d609630e89c2831f8a8bddeed5f582e4afae61128f9e6948f838cfb5cd908d15e1f5bdb45f88c55b101d5df727b247a59ba7a013d6e392e45b7a66e742c7d5229148578ebfcd46a54e6d1d7cf15ebd89967310c9495906fe8a797368fd0157c8c3064d7082f4984594aa6641cda36b1ba369d7ace4f08b4ea47f40df255621de3757f40c579770c5be3da6ab600f853ba344753f4842351b0a195d5eaa489bbd270d5edf72410d6348eed641184d5924a2fffdf343dd4ea8c286368b70b7dfd9adcf9c665194fd870a72ff9934f06b7964cf5b74977c21cbaef27920d3d8a3e09b6044f1790266fd7d0319b1d9e7f86522b36feb6a147496161baf7cf47dfcc84eb2d70c8a0e0047ddb27ac1e19ce382f9d124f19040434bace8dde3464c3411385bf197f4ac128ae7de5ab8be406b9846b469316e4029778a085e4ba613c1d8d56d431e25fc138147abcc7450d25aa8f9500e1fa1a1f8ac9181a859364c1d63620a35e45c7c156da4b50380242db4b4fb03d85b8ea9da2fe62f588344a9a811c3b963c9def83cbaebb43af5706cc3dada46d5b4de52045a54cd8eed8e385fdc7ad35853f4ceed940d622d1582431ac9b996fedda87bf457bde42e2a8056f58a29175f848e2abb470a1d3bb1a818ac774abb141456b17d6f4b52169b117fce362080f94d2e608340d7ecfd6ed6161fbc44e47d53d88fa298acd120cb8aff70a525bc889bbfaabc962b117a5db7d076deab04e956a965b3cbfe5bb8b0bc93f7eb7413c787c54435afc2ef2edc0e8748650e8f886aedf082437caa06eefc00ca2b71b87d22140c25dd2c7635dde0ae61a661253b1106d1f9e71bdea918b907a93b7c73376cff501bb93a1b3df985bc890fdcfb11374fba136004849eac6dca76224a9f5a8bacd233ed773530733df12c0521e4a141f5bdbb1b2ad747afe760d53653ec1581a168fc7bd2eaf27024df36196569e6bc690116acc5c6e92f00c66aa67a9f9ed7c807a96f2d5db3b107989facae2bdcc93c6ff4c7f09c5e6e01b248e540ec945892b3aca36c7276a9bc1d7defe4c503e131f44fe0680713f2e59506b7e0546450691de61538d1d83987f2d054a5cac1180ed4a182e99a0fbe35343563070bf75309ba0d3beb29191ce43156916efdf46068353fc7bd24fffe681f181c04d378f6df90b011a1528e7c568a06a94dc10bc37c7156d15346c3a245d8c26d8656a5206a2f63f82dd6a0ae0a93c6dc9fbd6cf69a3ef3e73767bf2939e401b4dc13b62a47e334c4cd636e7ee5201a228201364685886969139c0c1a21785e94a19c1e84ec50a188651696f1d3b2c31c728e0450d4c0ef1c106987661293342eeb98af32ac776ce08a17e447d46f038836c851e4a30cfe77093d12644ef4061c4bb5bf468450f3f51a6b9f5ca13182e996106b4a56756cfed4075b2ef831f9804ba5824165d82b09ade7b296beda6bc4fb9bd361ab8a580e7b069a9748a3c5cfc734a63813deb2171b93e024bf563aac008bb934954a2a97be27d972d1b37fcaea54119cc6e8acccdbda84d51bd99c0c9b79219eb723900665010c737c4bcd426a6fe99a0b4da4f4b936ab6bc0ce447ada1eca6aec570228e4e94b5fb5cb10bd09eddc968de492a542678d9bad8951586e144f45b142508367267463dd7a07d6f6ed3a376a2af9f1d373038d3e26be1bbe4d02c5b78127271b8adcbbbc5ae76aec311a40ef8416398b4f233232754a11874612709cd2c4f111ab3f21b0942a9d5c3e91617446f00fd8d2cb41c0781fcb3c71248238b9127e999ed6499443e6ee254218ee5019b9d31b16b9ed7f46452acd5ee2c078b30de0e92f3850a96df7fa5a232d5adfe33d4facd73ca4df8ea890798a79865dd9b5ce7063ee1aea4ed349b49d468eb7ce7f96cc43a9ff7feb77b5aa393fd7bd4039072d41b5ff2f5f8d0d97f89dd715475a0b51a158e68bb5fef4a6872e3be3a96fa57af5e7984bea55a6d48303054dfc1bdb058db39fb2003aa6edbdc156751d9fca5858104ee700446a7cffc94da245fd9a6ee2500923263a5ca27e1f66b7ffef873e62c89bf993a2a9c82e1b08c34a5f0a44d1bf6f3ae29687a35510afbe651c49d5d3293f0aee970f339f69aa96586e27190f3e17f3fbce95a452eaf619ff27c80bf00707eefcad4e0ba65f5cb89e5cb9db2a9f779b2cc28ceb905f34d46568d4722ab9ff5a72f2f6fb01d1e9ef8d342abbba267b4b4333625880d97005940932ba5086721e2152821868431482714576f7473a6391328684f13e4f8b295981c93f85c88deb86cee3188648e6da70041282c359fd8bd36f2de53f0e028b6df5ceb68a0c5bdb0917163391e42dbbf06264dff8bbdd16213b4a5234ef756d3771f3693609469224eb78cc6c882d65609faa0031e093b681edc6b989b728acbbe9a6162b92aaa6f2a1a0840d73e7e42eafceaac58142344686a20559b89b2ea267d1da56e1f85365753020c5abba3dbfdc3d2e15e49b81fbdd75871f4aac95b93f826bf7febfb064deb9fafce3b7843b4fd0075455b392b4f59bfd6add061fb754431ee5a23237b463920f53f7d70b7fc81ce379b4d007881ec6cc6ba05e7427a19134172de604497b1158dfde1208e5321a3b46a6e0f0a1289e119cbb6103b1ff229e113cdf91ea15c098b0d526685cf1ab181b7338f70f0de9091f0567c0de60957fdde794a620fcc006f174f6b8a80364cb3bfd18004034ed13bc3ea77496a3d105f4dba2e98444a13fcef70a41d9ec54392ca42cf816b65314266aca542ae7d439d9a8c8b3c514a89963b4796964bb46cb46435a9fff79bebac4cab10e1c3d4a543fa02a099fd4f3b3232744ef988d687fdf9eb1359ac3e7b96c4216bb37e0eb9979ed7152293d91c3cdf0a97fc12c5894f1908eb7f2c3f0aa930011da56674cb6325485002c8494bf72931155dae7739af2ebd711b97f74391479ef9032cb519477d1158a6e22a6e04a1240c84c6be1a91f311d265ca4594a371106b7271e96438b88dd43b06a0d15705d89f0d974867b5b93b4588f6fd5854fa6645ed3a7bdf876b34d98c90cadf806442bdd224f8cb0db7988cefcf610eb1343a18d31cae931e4bd1ef2ebe5f10c1462ef7472f7e47041c5e08debb82155726934090a1a85639944d92162a19c682d848f5d09ee82908b67e14405adc0c3a163998965e62e3aecfe0f14be74d2d303a316b1d39cd3ab466ec071b1941b896604477c693f2a6c3a7d2bda09e17c61a6bb1764084664c42e8b70bf4c8e0628f22b506a3400127754e2ab9cecd356401a4acb91a764eb75213968d805c5820d1ea9dc50328ddef48c977bd6c2f5932e5f3776aeabdcc8cafa661ff1f8efe052ed31256043d0270aab742a2f15e22646263d3f0371d7f10526c66cff67e1c513d9b0bb51addcec668b77918473889eaaf19c8a2261ca181d054b7e10aa4759d5f1c119eba62c442934b80a2eb7595ef6a6cc3e0f4d28cb2b544005f42749ddaf091026982df3644716b905726e3842fb60285cb3ce5ddbc9f55f4011f052abeadff4bb4f6c8cc16e111433c857d0088814a0fb41c3e267111e5cc1daefd67892effc8e57c83d37ae51cdf86c1fce0b1fcf03d218badef39e3303d4d9bd777094303505c508129bc1e800552171022fefc04a880e885a6c6a8f676f2b485f165a6c435d5ba904a68bccdc0a24a1d42e976fd33ce8df40ca156f2822f0ab612b6496e63135de1ea377c491e680b7d960961b866179973a4f7446dc2d2b98a90a150baa7cd9fabf3c6479dc0fca17444b768e23b2dae74e1496ead9852d9df0e614d62585796856b63a4aedf351b847c6f768553ab0db342f3cf95cc981010d0bf8713838bfa618ec0934543bb91734cdd0d536f5042a7f0813be0868a4ffde973d6dbe8241a560cb620593cd911e12e7f7aafd6b288bd0b29ea9923da003741d6fc7efc3646fdea5ba8141d962cf1d66906772c71e920edfabad0e5600696daf60eb83dea614cfbe9048c1c7405e27276098557aa02ced31aa907876aa991f1b3d4f79359914a1778a46f867285c1e1c3247e39e4f4c33292715790cb466aecb589713150bd3825e481be53ebc18154b6735fe338af8240414cb545e2edac02db6a8c6d1e50f1d1cf182824673a22d955aa10e4e5c6496b068c9b456a51cd217e270e8e1cba361129d27d7a4faabdc2795c3e918c85dce5daed2ceb39a2f8ccbfe03904b922e5b991f7e8cfb0905b6949487f261577140b48d4b510472af9aa4d7355d3532ef72b41458eb63dc8a37260cb40a8ec14591b7bf6c1fcaa8a99a40e3740ccdfdb926019e3a4fd093a2bece1f5fcdd0e5050bced1dc7192a51ec986cc037370607cfa9a6af9b0dceeaf7a6dbea1a106f5f43083fb63c598b34499a7c35045786d940b45debe4782789b9d8481d3cffe8b6521eab9300d1787b8c80b987edec52199a8c56864f3d3d70179b2ea156531c339e28b64e0f95cefb8d0d7feb5004172b02a49a74f5590d372d276b81bfcc7db5a4d70fd4d00fa851a5a0427fddc334b76118138b01a4e3fbb803d2ef7e101e76b4c93b4fb4c7fb8624db2981f5408e330ee40c736c65c2acd0e970c54dd5bfb7909cb69eaa1d8cfdd64ba2cdd2c585674f933b9c5ef6f72e67776f7093ebc1c6a048029693c9ec638dcd14bc3daf023306a56efbfc08c37ea164e79ed04682d198b75fbcf42fed95c477e89d5537fcb4aedf624faa46e756ca804e580170faf38cc9fc153729a56efb48fc65d704047733e9831088667fd7290a4fa083b4c027351c98d3f49c62247e33c6ca6b201eacd3556c1dd146fa1afea10b3b9f7bb4e4ad23c02549cb881a88beb8bd0646ffae788b12c1338937bef88f036fb25331a54d30d8a4ec935175fa6f36af58790e9de36414ce141038755ca3212af21238b759c7622e07da717699e36eb4727bf1cbc28c0345db4f8cb8b9d9cff19a83fa198274dc33f40bb63d7f3e29ab37ab70322750bce440af392259c34453e0adf272710ed17abde6aa5aada20261ceef1a2a49cae3ed6561f96055f11106cc32cf54e2f47222a2e490d283fdf53189ace2e6715cf31ffebc814470be255d1960120ad37ac306e66d1083276c412d1317b012c9f4c76dc765a77d0153af919ca3e90f3b1ac3fa7aa3898a14329f99bd76dd66c13d50493fb32f810fa9fd53afdc9d642052b5785787934384a1bfe360fbb70cec3bd39e270593d1db0de4ae2323212f5693db59c7c3ea557b2de9e44e73f10e1586f4d5b4ad36eb16c10b19b7736af3f69bdcb126e5ae54f2e6aa168aba68080471fbbf51c187223de3fcbd6e578dec4604b0afdbaee7e8d5c3449bc263adc60a92bf61b480e7a9e4dc1191e59e068d70941f7dcf63271c66911e753aee6dcd6802e4a75cdcb9ca65e36c04eff0e56faaca0c8e3620e8c1a6d4e4c61e9291af4535925994fd73dfb8e7b07ffdf848eebd99f355657e28aabbe46673cddafd13b8090687ed754e02350e983b5a1ca4f4de36eec54cbe329cdca5184894ad932311c171b04579e673ea6d819d13cf2c2ded667917959900ffc23f48bf1a294e9ce9257f88c51938fa57bc799249c67d091a3e8e627ebb667ed7665edc537128b80bc4cf8ec6067d720b0f241bdc4da8491c3ae0990743bae0132ed2fcb566a0012ab000089bbd016da92ef06069712fb2c54e65f2b35ab1d896b9d921afed3df65a274a7f6921495a5600267d01e8dded475757f5347540625c40d143bfdb0e0c398409347137c3802ba39d9b20028a31cb9b046ef57f958998158a2f0770919d765fa722299e3d88868d4994308b094ead3b9de06c341a62c0ebd86b32d286626edec6823275ddb9dc226bd497b68f634ecb8ffa76da9e6513e413d05fe5cdde2b38851e3a4fa8df4c2ec903ae026ab4899b67179d73a730ffd6ecd45db29d22a3343063fa69eb47f134d03236bbae80e96beaf62aa2507d5242474cb08d9d29ba5eba53a59b9bd9fb5aead2ef530ed4b2551feb63671a427ffd31233ddd26204d9cc1f809818f07445bf6005182c771956ba0f45a528cb21538e94a33d88884b100fec7c744bb901c86850d75ed3b8ce6d213fec33d2fe70e4824faac9849305693005898a4d9a8cebcccef36be9f94992308538724f2ed367655b1ac8dbcffe20e40cfa6a5c2f88df5e1b2f9c1268127bfc997a43dd771bbe597d087543dfa14ef68599e196ce6b4762bc8f7dff62c3a2604ffa2e7877337e18fa68df888b5775ab3bec793d7852f2785b38d479f874c358a2e3c5fb5931f3fb9bc916c88342dc6f979d1c54b1d2bb02f48611610e8abb29351f8e234873a4a8d4aaa3470c10af773a74367c20bb907390137e494354d706cf9d2f2729e5fe3352dc0c5fd99a81e4a8bfa0fd3d676663903ed420af32bfe6625f2cded0fecc21bf6953e6984f7573f0316b974c2b3f2efe949881728ddabdde0e0fdb0a73c03042840c0eb4a369df5636e0dfc7794eb2787152134c45164031b781fdebf6df237f31b6ace87db2264ae80071733f6490f803ecba8820387042c81383b3f89bcc4492623727dfc70073a2f4078a931a6d0ede15fd545c7c907126498dc82ea2e24b113f056eb2aebdf1cd43238df63e125eddfcb4338b387f3b2fb82638214ad344307e05ada2469f230c7369b9fec80dab32af6bbebffaa60dff14d999697992fe9fe7bee2f0aa579a17a02d1e00f3ee3ca702e23e201b893e758ca07ec72a66e87ef5af39a1b3ca9482f8bb45b2c216790018996c3ae185f7e83cdd3186be8e632aa3dcc47a8dcf267ec0fb359d477175593028b9226c9f8a76a2f445158a39f2feeaebb40374ae4f84b5b7e2f67c29d5c9e5d05fdff8215018ab86e832943b7dd4196d04e2d6b34261dcb9c91c95fe608623abf923110c49852766cf82480d5152b2200788c874118baa24eef4a8effab78a9846593bc37f2d7461c70bf43f92224b138ce342c4d984b66c1152f71cfd0e842bb8199aaa49c8cc76f13aaea3083802700d5b3e362e0e22a0f4b645463864cd14606107f5b58a56547787ed135ff29a8ac60e5b9a0b5cebe0f7ab0561be6083fd86942c7f594fe90525938d34d398560058f90275c393a13dbc351e8040aeecd9a026992a804bb7b007a31cbdf60c615b03f931c228f2cac8651f29e44b4a44fd8dd60c005f5af6af03a13d0a0780fdc2167188b56b8c84b535f547793b2ab2d465787855ad8ce4a34b8b48f2197e042bea2780a4ba9565316ecc4fc6c53ee756c71a5cea8bbf90c91d0c1184708974036e0249c92ffab3b827076a2eb884597b52fcc0e03b92c5378a0eae5cfe66675927c0d2e38699a9d59a1394dbe943e28d6c8016715c9359f33fe44b4590b2068dac83713a7a5807b250c61b3964744364bdb7c47fd89bdfce82e7d16cbf4a7d0408cbc6b307008040f531700c38ccdcb72451a81d899f6caf8a0bb60ced627e73bde106f7180242c3d142091e2a9ee5f0cdf5d2cc0719cd6d16b8ed4ee2f57fc411a0b5c4755587cb2e45aa42cecaa1b753895433f24075f67bb05b069b64de738cff4e81289e13c306a58bf1e25d92880efd423def78aeaf2df0da895ebed5689cdc499196c1e78a1a10fd5050128ceb8c3d54c487a100dca26643fe37039181714bc7dfeea4a345a9ef8b78c046ac7f6364e757e61311212323f8dff62a5568127e347bf265619e48138636d6e8c25f108c58a0c4788f49a7cab3b7250495fcfa52df5c1c5d90ea1df3e90a2c399531c265b7682b70438054248504d320637e42240adbb24c1376e7f20f3f27fa5e3e97ad9c452ea2e90119709095a70c4e0889fa7bde8ff8ea559ed4c0f213b844a08b1a31763e20fd4fe83989b81f3592904c72909f0ffdabfc64a154d550f88b9c0c90e7c3ebe65afd7178468046dacc748a1bbaa846e575fbb60edf8b03cffe070e255c9a04b816ce1b69e86cd1eb9029f5f2323fb2cbd04309ab11242d2be7346ddd734f4682045076dad8bd609c1063d5621dfa414ed5cfda1814abb554ec4dc8348496a1d16c7527dcc5015a5600be0f9d6a8b824f35c700a803a8dd938008e368707d80c5382c9b54ac41b240b7499eff00d58d9a3bb04c0262b0bb80459fd2f9a089617a9fde399af4645ec823c086e8f9bca3c069b46519d2ae7a145fe8efe6761b711c785dc06416a45210fc908530c182ce7cd7e544c52604b7c0bae1d05cf18bc6d1e9e7ebd9c65a35ca90ffaa747dba18a2a9c36dd1977cc1785aa1abc16610feee05eabb8663bbbaede7be3f7664656a765d3fe1973cfb49d27522718e275f820f7e9524aaa85af7802790b62907976b435ce951cf8401e8625323f56f2f7e535d4b60c51a95fc0e9d7f9ec1496445073e824ce3f7b41eceef19376edbcafeb0d92d3c4861db19fec6ed21152d4f5de64bbcee4688f1f7fa7378c169430afe5d70f930a9753b0c82be5e6631dc7936444e6e1e6b4b1358e5dfb7bebc4c3bfefe07242c66313866856726681c61db06250318bbfb1b65c6697a1b79d34ba06f258653fcadaf214ebdfd6a4131f01b6d2447c1146b6f5d11f72797d00004103f3c7240eb0d79730ffe35168f02bf76d1e98195bbcdc43fc384598202531d21db38cd9a58a47221ace761902d21cdaef8bb27a071fec376ad0a08f52bc68e3f14a9b5f8efdd77f7059d6338be55400c0ccaa75b3350ae8c460351dfcf73695039497ee291c362f6947fc71c1b630398e89658eb65a0b99ed34125bd01b7454a5e6c22aef45919b32b608e34de536a75048bb2a9b9e8f8e890953439cc4c866caa895bf87705f88a5485d4ede6471189f53ef3373fe4ad3cdccc33959a29f3f872b56f1c6ebad4241961718c1a5014d6fb15fc4fe6c431a7921aabb323c66ab584f556e61478afbe291ba2d9d662729a620a398c5c3d07198d1fd2a3f15a65e9f984ebb2829b0ec7e64ade21de78782c379dd969c25f9c016a1b8e42313f78fea545af76eb738f8048d1ee93570b16c73831ad57a8b4d615e9156cb62195e0abec75c70111774f7105310c729dada138b185058d15eaf2693ea684e41455d38eec152bf3731ecf50eb2232ebf78e10acb78f9c1e66786844f74295d50aa5cabf7e93cd148624d7d86dabed10d216ac951731f5569959c29df3382b434a8aefa448fafe0ef79d484c1acace56cec7c190ff96082c1b10d4cbc1bd16a8cca4da12868c87d114d9e4cb491706d21d1b34f8ff7fd6e88c659b6d48f09aa457444f042aaf0f6f575fd8e923ef5dd154d6f996160c99fbb810b0f8d289882064d9d256bede0e1dc94406a6f7a229744fbfa8914115eda4dc24f906cc1bd659b824c1276dcb1dd8639dc6646e55456fb33d4beaa6a524263be273fae921856ea76ef0f018fd66709f6278fddd0918dcbca7e68200fcaa080f9976d3a6b5cafe0a5b3d6596a654a118fcebca94df509a8e9d1bd594052f0ba10ed0765f613c3508b5a126c431087d120ab9473e2c8cdb5526cf10b19fb5876149f568c7fc7742898d8c1de962e4e828409cd3e7479e919ff6abc3cc05f64c7505ebdf3b848cfeb494be77f0600c7cb0f159cc27c845f0a6d846808bdbe99f580429b2b4643824ae7ca632fac240806195c159dfc42bc1801776c6f1110c109ebb4cdc86887245bcec007f470e46cb41be354d78996459d667170512bc3e777b9f203452aa69823fdcf74b93cb380e31d9e4f59ef7d5ca0a542e91ff2ff62ee05eabbedb5da5713cc6470d897b667c86e89e87836cfb857b891b2bddc0b17108457be4a5657029fb304eb65a4c2646e42b1ae5b813cb9afb7fca36f05a47df77349bfd054dccbae2e39b88c5edcb8cac7f0d559fb6bde3c2813d1e6e5682ecbc8f8e28345e061d1f65ac1c1691954d98452cfce277cc04755e4abf80525ae6a6ea2cac0807673783c9bfb6c3dc73be0a28ec1d570bcd9763576479d06677be963848e97781e197a656047e22e8e9a8db1fe29e570930ca81c72c50b67f50c0ac0ad8ff7920b93cd5be3a7d6fab818a6135f28525e3fa7665777752c9b962a829f586db16e1808ed474b44f919095bdef995a84a0555d863c670d25aa87eaca3beaae544b82c37beb927f0fb451ead2da95dcadf97d2da026f2cdf1afcbacebe71ed992c6b19283e6c12efcf3ecd5935a07202fe46586ebf60ee38f644bb5c22c57d1803a735f9ecf5b9ec7182704a4495b1e677980af7c0465a644bee0bfffb6e6247d75832c07f3ee90419c74a1544b188de5bed8da6a05b48f741c6fe6c9ba574d66dbe0adf34f08576717e343602bf4350c2b6b55f5a0260f5e266043f35c0230c3191fef9d8b8d388215c0f5288237b09596893f5fa3f073a49ab8b312a8d77f636ebc7c87b2125195a7e6bffc9de994994cb53b8f465d55ae100f259ba51881d1902cfe6a7c6b72e9c49bf4cca23998340d15ba17cf9303bb18ef67bc2cfde07b891ed845a5c086512b592a9107499e3f319520a56cec374ed22bfbc62f1d01b487ffb5f749fab9382bec6fab315de12b50109b0b71c351d354e2b8daca81cc9aba0ea35e3061572f0f7791e743fc823c83a61fa79d53bb713b920f6193b8167bc3e66199e32834016eebad9198e3d45cdd03046eba55c64a0addd096f6c346cc9f021df1a136c19417a45704211dda50fc95f4ea08c91d1d4d4b80ef9ac7cf8ceea2f8927ebd4dff4b1c9db5cb8014fcd83cf48c82b2c85c4c6ce4b1751e88deaf3d25c5f860ba58c808cc0cc91b0e2eb6f8af7220f132e8d9ba832909ac0260ec9d57515658eef86e281b2a41dc4ab68835840378810a0fd69a03cea727a8b1f5bd6a48f5e8d1d106fc53ef09ebf25a7cddd81bf1a25a3f844514ea2d08d05b0f40d1ffee1c1092f4353f481c53b2567157db259db6e63cb7743832c341b7862cba4d77cfe009b769fcb08a10c46f7428b38d7f69e186fb063547fbbe01347e218edfcd9c51717ba6500738e9bb183885205cf8e374fa6ac82a1c6c920b119095a510735349ab8104000da2fb419f42d161e65230320385eda5d09d1fae81ba0b43d32001f77b3ba2d935b5e2c0bc65551a25ef5a1608b18ce06a42e74d174555ae07942e9922dc00268263a4239e83951e0d8c594d2deb2efb1ac946a8a3b81e6345388a295a20b5582f92ff703192b583bf3636484fc06066f1bf3925a14d1fa4902df52007cf43067b3f791bb233fd74f8a1668cc0e0811986a343214c1819e713f0c27560d5dd74645a3c766c9d21fdb4ef62ea98c8d95720c7d6d0ff9e2a9d1b92adde9eb25633b3278752670874d2cbc29594298c499f3d130a66c7cbcd4d33115a8c671b56157e97bc1b67544999e3280715cbd6c61cfbcbc49ef3691762c2c91f601b7ffb41e8b3c9edd5d713b53c58aa3d417d360f1fbaac2e27a7500849080184c9b8acecf14f5ec1bad3acb20f1893387ea91705b8a4aedd8b8a79669271823ffacf0352e7254ecb8ca476f8878d917d53984f2dd45619a7df3dc6470b29c246bd09bc44a9b5cc3abbead744e9626f6030261db4dbc19f147f2da52e1b4c4dcd04500e940825af7e345cb9f7d5939a2cd8361ca295b0d558e79a525649e55fbbbd51811d215498efb5c6b1dac6505ea2ce9d6581378f9c781c484bd57248ecd55ab988a0a54e441702288a9cbd46a4fbee014c9cb84a2b03bf24f98dd29d404addf947a67fbab056fadb64fe8cc5eeffa7c2a2578fd34f55b671da686a4b5922c37c2b594f6dd0e483c7df3024cdf5a513a51b0b3231b7f2ced96d12525948209888bf3c8cdf6c969dcdc777008a21e0be8120e4e92e635f529c3c2620004f99813adaec9b512f0ecf48f0138548b136cfc1e90af7f7230de5fe86c29e175f4b83f1eaebaa4f80246c1d203e3c54f376e6460d3b20a376f8c04577d31203825953e523b902081ca85479a447afef308e35e1f73210202bd85aa9e0b91bddf59bdbb5d730adff2d00dbda0c6645b1e1af6684c74d319d5d6e9ad46b8e9e7bf21a332f3f1fbda6ebbef3c20288aac7667cad2ed897c6706387992fd31d82f813325675c84d5126318e36a94de493947c687238ea8c36f29d46c4d43692b6066198b65ee8e42193594be3b97ab869c398468583c7c651d2481176e7ff5f0ccd74cba587e9ffdeaa9919d5bbdbe99cb6999e97ed4aa9617a15a052bb726d1191e73b0779618865d51a4dc3fcbbbf01bca5252fbc7a41c1150f09a0059651a6cf9ab64b1040775b987a021bdb01b7afc5bd4d390d1317f28bf4366fa8f08e8b30f6b75f18f528af695fe1cb84871662533e543e9f20b9ac1322634917e599f1d590d863e741f4089ca8e7bea2258f7f75db62700fac2926de6284d33fbdc520d37a38e9f433986284ac144485d4050f3c13a985ad64434ab197515e698477351ed1a0d595ef882b6d1fbab5535954196dd2d652a25b096a7d7bf1a4bd905de4092730cfd5f1cf0daa3394c5169b0aa29177327741ef64274b72f913934233d006a4612e1068d4106232e57d6d7eef7af441c380ac03957001ac824fd6187d4f63d5875220d29b771d6aa9f43c295095f4543f1a657862357deeb429e43ce180f72c55e27435bc63bf96bc60f1aa5240cdf232bf4892437198091ced334c5a2e1fbf790c590dcbd85a57fd6d508e1f4d91f7624a2d968f82c98dc45afbe9f18b530220abee0b976ae18ea21b1e46a37275b978bef3948516e6d84be0c979d89c0dbf7292fdf4111f5338bafb8a76651573a5b34b8e2d2df0a1cec685888da3d0db54080e76801fb86ae461af1bc233751b09a5c4a66ff4dd4e23af8e9d9fd2ec6be3bb32b80b78c7ea2d1b44154792bd4e73996ca66c9b8efd1b10b6f20ebb427c88d2302df50f19b3c4288a9a8eb957aae18d999f5f5544a6a869aa47f29b42230d48d6a5055f4e6766e254afed2a73d34078b4db389ca4ff41408baf2d365bd8e1f5d19faa45f5fac1dc5b1ae335cd8f15bce20be4a2add6bc8e4d6a107a526bdafbefa86c4f1499fa29079e00a0d488bdda37f938354482e4f2c93ab0f5f87a2614fe70508c7c48f175edd9d78eb7ec5d80b7870f20ac7782c39b5d7a5f6d01dd5b65d209adb65f75dc6e5fe16a75d4902a7c5d5adba77c8b5b1035dd3c30bad02319080c4aa4ffd271c23e472b1b98f4f8283ef7a1882a33bfc71f069fbd5b6ea1b24c8e6806e858d1ce661f6fda63d320458687218ee218bce367b288fac92467eb87f0d3aa80c038e9e648521d3afdc253a10647b6b170b40a597c4ad65445f8541961f78c7c6393cc2c7583bf65fcaa165ad94fbcfd8b1e2aa69ec93d0ac48142a8056bb517f564fb280e09ea53c891494957d149ac8ad75b29ac2c901e055d809790a911395585b834c0f98aa21c2606180e46ee27cadf73e4d2d74224ef26b46273e528aabbcdf938c4e144a45728d290b4389dcec03141fbd1da7ccb109e0eaee0df6a06fdee406f0065ce6915e0361d4e81cdfc59fb0f5310a29c1c8cd04a90065258cbd61d0b587ca4277b5e0b8d997a903997db797e5dba23172b3814e818f7f3ab0bc624ff38f61b25bea1aef3bd557ce380f4d6b63612e72ddfc2e3c52317eaa4ea940dd5c7b08793ff113f4f7d0d00ba6f08779366f568548c33b49b03e0665fd4f9d04fe0fd0267b131cd2b47316f45158f376f11cad6f66f6c96d6c1be6e94dc8a6886fb381791d96872e6542f0e250be7e5ec22840cd6b157c076e2e4b9a527efdda6ce75475ef09b10f858aa9233e6cbfc970dbf657d5b4a97ff483f55beb7f1bd427e064b12b1c4d07846c0707b0112f6707c08ad1c4a170df7379efe63377d84e4b4ef38577796262f2b72c0a4d970a3de1005f0ab29de16d3ef864d60ba294a3b6aa78fc466466db17603ebbfaf4e1bac9c550e31cc86b55c5517053de2234630b991fd77e854f6df7ced57c905eb7515cb8f32857a8fd1f5513e647c70edb3780f923af431800aa132c17d72985649fc69a9b3e6bcd4b120bbc1eaadc8d649bc2c3056aeae9139e7b055e8db7c00b5a9ec94fe079a15f98ef6652952639e3b7f9bf8c785fea2d6aca27f03f2492b28545dd40c70dfe114d11dede5b7a054dbb4f05674212dbdf3fcf16101924f82ef137bd33b558e0b8f4919d30e0081e086eacc0abc071ea4812e3521ebb002ba0832323c51cdf15e743806e91f82011824397f0d3e9d8e522c2a23515136056d9b58b0cdf74835154a15631825e76c87845351160582930759af5815fe7b6d9e50f8b3255e62db5099636914651d75d2ab7556da1b47f2580c2d281ae21226f042a2fbad5536c15c5071f7c42c34f742ec4a8833e5b022f57763f3c1cbe853843226a8886f2d2703a0209412d45a7b08169b5f2cffa4c6db2fd08f3c7b769e7a6d7a8fcb5839df4ceca3ef7a9c13f610ce755e5ff56be58a7de187754e2bd11d6dd07a7a2af32d479220eb7b273811bbeaf2b5cdc1e1f2351c8889e81de22f0c34b63d4198231e8821fb22b780ffc326a9ada42073823a58672aa853ed9b458557eca94515455a30747e1a22794d6875fb66f7a024a62ae6770e960c8d5f6d39469a5135209604e4e250c8b560c07211c083d674d21cd63d240a38e381c87599fd386e68e3dd65a0bafbb3fa9b3a7176b68d94a0e7ea10865f18a3052097cded129a10bc8e6a0adffc2fb5dbbf4a0c39fbd2bad19a655db67d4b9193ae9466b60736e4f2cb8e4d62ff4d55ab7f81b753bc351b2de9c45bbce00a5cdff96442c6bcf40ae69204d9cca14180dbc83c67c324be2b3aa7351a6672ba3e1771ae3aa288868148420361ba6ef0de3c6335c50f159f634acbc686a0f5045d1c9375899b3f63d49cfb5183b617f687f23cc4b66fe4188fc615bf1f34489d3fbdc99b6aa7ae0335e6b06c9ce4ddbee90432c8c5d475631c68520b1f2e61daed8f118a1bc0e658a9e6dd81b15076c55870c269c5b7a779a6d02069f0777dc37ca757fa477df4e4c6aefdee3141165d084d0eacf39c2479a97af659b241d36cb1a009ccb9a17b3b24bc96049a928eff42c721b8ef8314e6276a518ef34540c87953b40bf98f9ead3ac28c151eae707e70d0bf8aec85f2b88b3bba60b5514e11464fc0015948eacb5b7b90091f2ccdcb53fd98e1cefa49aaf943e50e32212eb710c69811198d0c3d4e7965d60e8ecc8571f179f11cc9a8c22d466bc559ba4e4a6e10d30d3ce5d74cd5da50ea6e48ac83968deac86f678cd7fd144d2f0f9e441853802fc51bf9ffadcf76f614ab3a229093310fdbf65504b8dcac75bad18c5e7f5c3cc983e873f6e75f032232c1fb4572ce7f65ffc7f028c0cd8719d7e60495113288ad8ce07f204ccd037cd6489c905c87c17b39ca4ed81598f9f14206ed2451060261150455f4969d05c6ae4818e86352e09fcb20ecf74e36afdd76642fc286d567f6376810af6b3f4f07896ee0c2eeb587b9cfaea15e6f1b3c5eba76a47d6bd868dda9980cc41adcf451ad9a4efd35bfcfda340cfa0e5512620b2687cca72eca431786fd38055d9dd10a21d5a8dc573840fcfbe30239dd91b6e483b88642969e4e3d7f6a59289578194f473533a1d32130e521fa4cacf7790457b05b01b069418f896a049b0f90c6ed5403683c9858f8ebf33151c7454a5d99b4fe34b4d8f5ec54fa9753b2b48dfbd58589a598d24fd651ecddadad34724333679ff47f57c428de1fc8f8f6f51e875c78b81d607d40aa024a87029cbc3833271edd43374cc20c1c18940a7611b0297cfb728c7b9aa556986fbc888151623896885b72a3c7681b7bf18256c1c717c02043ea169283c07b9b8b78bf82f3e5377d90f4475cdc18ee4e971a9c2eec7db08ad280982b6a4135d0b64dca7cc678f3d6e64e5305e4ef3c0538c12094d58a10b35dc62727586af56f676c5e75ed04cd7ca1cd69be7e016607105cd67f9b9596094b201f1de9eb6c5564fcd35052b3c3d226b9dde0ba2c8aebc6567906cc0a8307c45e10f69ba9375bae13cd879c1385ea60381a5fd7a6d6dccad8cd91720c4edea4911a85e916b88082113f9fd1626e9bbf22e802f7c7faef469f2b959bc7a04165e92aad5612127d12f0eb1f692d9d292ca271fbc8cdd3491d43e7dd2e40c1cdc3c2ad9302c0fdc7f677d36a38148154aad28e8f66c2461c25caa3e6e23f3b009a6b33044104982bafee99c8642ad333b9ee060856669e2174b9d8c9dc0408becfef09354517ab0bebc0ae28a8a63d06fc0b11a4bd0faab082e2695b96e1527e70bfe363ae2b18c5a0c3ca1980e8a110cab7edcab736ad2edc5e1bd9df895bc0b6c70b7d9159e0b9c849154118b3cb24f5543b01e090e0809b0ad8f1bf79937c849f0c0fc5de1313d36b4571dc33ffc59c57b3a2bbeeca0e4ce3a31a3f497ff7c3cb366d0634936dfee4fc26046ce4d8e8b9c3322269c67c0426d969a0e588d016951427272d91df9fb95262cbb7337d3d3935f89c99be6e63e8262f1a0988c87625979009bfd80ceef4a089919ccc650fbf901285cb891111956a5feb637c9c2c185998511bcd0cbd5f28f2b8ec35cd3e951423824579901dd4b2cf8f2aaa2502da125fe6e127fd6559f24e76d3fa94a25b1e898fd6ce66ec6ba52533ba08da9b61574aaf518402e8ed37b6e50d6f5996d66eda5ffb06474d9900886d48512353354f44fff355335323461dde7256b0d8dbc6c3ce15331ba88088e6cb2baf8c60ffab0614055dee22f6f9a9146c12ec18828357e369a0368815c0e93b3da9887f9e83107b78ccf48c22ea6300c21f716ffa0e704ace92b4ab402d97b1dc6475a4c9ece4ed6606ee9018cb63d260cff024a52af06c631e00a40a1d737715eee40ab22dcaff4732e1eba96a379459bbd67215dc707624d2cfe92c607f9ef8cd9ba082db4711e1c2aeae03dd905cc03983186d7d44623aac51d8b0e4fbf458a51524f2b99e9b7be5f7f8f18af4f85ec02dd6c76dcd58373c15c7f924b9a392f28b9e6a399fd8323a0a56781fc5c405dc49e93231c487d0d3e2f14b09bcabea51814ca28a9628e74980173924653ba1c114f0260afc5fc59adf842d74fa1b0a420fe583fee4fac6261f267f1ed9c87454d5f5299d5a441753effa99b30be98eadeec599613af3994e1d0eb3cbccbdf56006dd43903311a1f86b41d330596dff7e17c1279acb7958934f75d1f2f0a612de1f8e75c4ed20c24293318e1edbb30251a581c50649ce8853168746ac870192a66adc254ebbe506dda2d4999bdd558b410105aea6e6b1d8b036d1270a695af84ed0eaec62717b199dc49486ee4af29b4fa19a0118526816cf31ea663ae73083f7c3cf4ead3894a48228f09d6378c4482ce3c6fbb4148c7c67f2f856ad30196d092cf51f3cc19db9563b85b2c2d94b377d4d4a109a625b67b6bb84d4beec93da7b4a6926e92d41ba950e11238d34ae00328c9c6202aba479714e13f2450ac5d793df13455de98b8709e912eaced59fce581f5949888758f4adaf2cdab0223f133fd14819e755155176ad174d61c7da0f270bb24470acf414300fceb00824d05764f631605a4c5de377eadc3660a944a968d41c6ba7695b9df6b7e9a9f3328bf20c4231368ca20b593b0e42bd97772ccac9acbd037853dda0cce55d4b933bbc1f0137d2d1e00a473e7d164598e95e515eb23a1b8441c7cd29c57743950b20d50026e7863c5d476c21860d9196371105c0dbcc519667a3c3cd9716f9787d71494ee2350041d53e0caca2bf487777f20a135e10635a9aeca42ba9b5ea7ba82972ca5c29a26aba255e89db1923e5b5ef77800343571735155ec52b719f2dcaceca3a574a02ad01d81859e25ae2384784616476ba3de6d7aaea3e159c853e919718a0e2a1f089cd432b8d44d93bebec2f3dd0701db9d86ffff1fc1cc324ef5aa56db94a1da15885c47b43d0d604331f30f6d22dac5f6f0690db8e8bc4f1f8e0ed508af5fafecc8ae8ec465681c49a0b4d39548a2f57c22b87d740ee77d0b750c579dcfca44eea15448798e997e050d3ae3c1cb3061cb04394650698f80d7ca5f126bf0b365b9909c40d54875357a002d14c93e03b5e57d16e9b8ce8475930aa8a1f2b3996034684a0903ef7e56358bbe52673849d7553921b1fc3c405e7ea713aea01352e37b0f3ba5fe389c0d8b6afd897245f7c9322c573720f5b70015a7bb2e26732de1b8e9ac6c7a736cfbac55e92217cd3f74d2f07d00eb97470ddec9fa5abe297fc2782319647f0bade9b230ff0a1f63e2acccdf360d0b70a01ee07ad231871df2d6577fb49fccabe1492f9a2d3467190129d94b9c6d7cf9d75f8bcc07f57c03ca8433a7f8e50c34894956b5d9219f2dbb4837252d9712e18de94f52f344b7c2a87eaf10c3696f6b774aa078896887994d1d2d97bebd6614006f6658bd045ccf6f2ff80312549b32eeef43167feaac1bb9506084e57bf581412a26d6406ed35e8fb38ca5b5c0b6ee9d03ce28cfc5d5852a1b0ee3ebbffd001fff0010fe9b364c109b886cf6c8e9dab254f5e8d9a814a13dd1bfc21a7f318e79c1ad6875f3ab65238ebfee16b30467063e7d5c92778bf5279b836ce42644eeac7c250f08e1c63ce898e497f46f8c5b2cd7a8f3b7643e1faf6ebf618897b06b34d203810797b1c198e03163f52baf9cfe0ba06842748b79980b07b2f35a39e7a48dd35c21d4908fcdcbb4215255f141f1a6831ec84fe206ffb15337960f58978ccf8c696e8b83b971d33355a21aa94cc849be751ca84ac3792cf71379878823be95c206a03e0f060ef0861ccb96384a79f9d44c601dccfbc353f4d5f47351b9fa34f6f56c3851aa1527dfe573475660074cb44e718feb1d197ef3cb314a1af275e500554c503a65bd7294ee4ed76b6c7bcd0ab17d0afec736208cc12d5b105785d70842e43f88bf7c53a9445daeaa1d5bfec1f1e8f08f268af216fe39ac704fb0b9e70d03f18cd6b2cc1faca976e680ace595a5a5433ec5dd48afea455db775369a4a53831ce41c175b929946f7477cda6302955799e3dd2656af4a46a84c71652286a97ea0fadd74d96679f0db92c529ac19187e0f922f1bedd171f0a0ed25a2a24be538489316b04bfc6aaf1996d73ec071f8123cedc09442bc4f44a195d81fc535caa37b1fb2e2568e4584eda8e4895949c04290281182156db9eded814c46b83f87dd83d0f2efc67e437b787493c6a043d4f944c7aaf09cbf3823ed4458fedd23ab1e15b367f798a6f223e25c96d0a4c4d6ca8aa97354c9e33fd2245d24613ecafd1b0022bd99c95d18ce763b0aa31af8b2191d8803a7314b23cdc153db41845254219350230ee6c835c4074f5b6e1292ae321e7aac5d893230a2e241c55e485d21e02a0847fb060fbfb75635888724a4dd383103920c52d83d3aa2b7c41ad464e2fb999e5992a3ff6aa2d70346fecda9caddc36b27aef2f2418eb52ffef58543dd2b5ef861f8195d28703d257eb4a645aad63ea1fc38fb14c60e52246ef6a9fe4566d9496f39babe6bdd3486fafa84fdaf704987c388ed1d839f85bcf8b6614586f31472de9c779d8b66b67c507b159f8ea66bd4f17e34b6957e3ce776d45e53c7c2b4576d590c1e337f70e5e3af0f02d78ec4c95071226d95a36e16a666280247eb4376c4a87a6b534e23e17ed73e4943a45225799f623297cf6e406a63f8e26db7b1a653f453e4fffa4d26dd7657ac0119367d0c537a1ee807dc3491866b8a5f70f7d5b4187cf22ac89b22b1f38d3df1be24892816ff2d9b338fa032c44b0cf13b4a8b3e884259525539bbf63ae1d7e50e5bf71816fe71eff3401d2e3ed554530631bf841c1fa2f0ce86781912f9c6a7d809c617058f950f36998b1324af2c512f0d76733813a177208d2518c7f21abf6258aa36d8af7c6a65831db7fe02e269d911b557d2ae472b3d0eac4cf2cfa6c2ce4d349ff2942dd8e898d0a530810384549de51915b19c183eedce40405c3908005d95caf780cb9e97f471a775d73af64ef6bb52d1413766401bf960bc603cd508b4c9cb2bc48b4c30cc308f9faad6f0d40dfe9d7904fc9059a760ba15b4f5eefc2fde6b28eb1b441dd8098734b4471fe9273760731c35cf7a033f6864e26af0e83272907486c849e330629130b70fb0cd544571361bd4e09e81c710ddcb85d999c11695d80e52f82a15c354c53f6ac59219eacc4f488e489ae22a81682dd47430214fc59f15ba9dcfd33331b036267b46caef4b8e010ff849f3d6da0663d5a51ce9fd5ca17dc569870e02e66b4c92d7ee49c63b7832541c9f319164ff2653ffc70cf4f16d402d367e318c1d1379abcc354313c63f81fcc5b52178b73527951f6b85344fc8b94eb3b7608f06fef30e814eff3683bdc91b826d9bc9114f0fb8a5fe82b13473bdbe312088482ff1ed3dca292a06776fd96a81679db5bdd9e3f4656df9bd8a3112c79ba45f41db2a5c25f7b3fc5958cfddf21a8c3b63bc4a49336ae79bb42074360da1b8049122e1b8268f9ed1e6a0b72036327e9536104bf56889637e375d2db0b4bf988f467b08e6abf61abfbd8b9e0e9132b6c70eeca84f10e1f30ff53018a4a642bcece6377004e29ea85345cddaec2d251017a307c0fc4252ab0d618c4a2cd820c0579f5fe59413437297b50cd55835e27a4cf144f6bfa2a166e39ccaa89b1f29d8905f30cd63fa6007c17a18d6668a3f72c11d8ab727d0257a533191ecbb65b7c26d511ac81b57b2bbfc888585a7f355473fc206f926f9bd10d826128e5b54a34904677eeaf76c21bf771ea9188e32a73931fa85c4e0bc83c96cb55f8bd67cd4240aed509078c43a8d4cca2b8413cd58ee101ab797645e2546cdd2c3747d6c871830cd728161ef9dc94176989832128984f3123d07ccc6bf8074ab796f2b254c5f3d2a929b6b26603b01185850252940219f2eda5c70b72557bf04eb2fab7a36b905de5c6aa07d5ba1fc364c76bc48abc80de148d30c90c12e82607b002ec80cc8fecc383f2267df8055088a815600b2bf3ce412df1629eb43c484d0ea750fc5f618dd1065b65ccc59a25cadf0245de7402b75d5620b22d82fb3ff3b13761ae4897358ec300ce7caaa1706aeef966f74c87d15168cdbf490d74823b9152f131d211d0162c975c28261c4dcf5da05874bd1e76c8f148ce7f7ab3f6aa7c92f90076e536bf8f3da77f9a82740b32a053ce03b88b7e839bffbcc493156438b4fe2b9824f9fbb6deb9f46621f72366b525b7358dbfb822997ac56c21eb283ae11a00ffbe13d263aaa0c3ff5c6fd7962de0eed08a65b8d96fcef24b9b69766278f8fc5da2ace67e81959589b441d3b7b6c63e1b3f44ece4685cb1a4926a659d46d1781379b8bd43cd0556406a9b5c8f2b401baa48bff5705bae906296d9482579962a44184b2b6ef242b45fea6457e448013aa596e8dc664a089ff99059958f766a44a92e40b52bcae2f1e1701c9ddf3611d4871586c97905af09525973b0c0b56fd2ed4ad6a021482e7d9286caaea577de858185d63f0a20a2e20baf6ef4ea1cc0586893471b5234e222e6126cd360a567a86b16530733eb901f11de79c63d23d9d0b8a07e15af7b66b413ff947e1bb65f7dea069677ac1f5af99aec1fd134cdbb12f429b3970e8cf2df13776fcc107b55fd06d80051b3a0fb766211b1555667123be764a198b59a4bd2bcd7dd3e37800ee2e64f29331f3981e34b2907d43337f3e7bbe0d00d14d3130799fde97e2f990763fd6d65d3b336a6551051d0a2198a61a224caff516857dea5d0a80c14314b834690cd62d4f01c9d4e3efbfd7775b0ab8902757c80b1c7598de5a10c650889826f4cf73b54a136be74b94c243fb1dd5b3ebbb49e52da77403672559ffd6cd03e3b03722b5f5e4668efe971288e021cf2ddf7279f479bcb6aadcece4a5740e98011ea9dcffc5dbc165092898bf98da53b226a1cd14ec46e207105c98d7f13133725365ad677d788848d8764657aea22b1c77dd2d6582d597eca43124d9ee16a0b2f5b44bd11d881acce8ba7f68faf906ef842a56e136c2af00fb23cc1a1a16081af1eda616619aa358fd46d5e8842af98362730caeaf469417b5932498e25fa8294d6512d8ca4ff9ab90df81bc2865dc86e2744c1e63425b9996cbc652433d3c4da568839225077ea5553f3218958b888f3645babe318b05fa6793f78caa09594b82b2a930612f3f2600f22744d580b28a49a93521642f89e92b9eca2b2500c9c85b24faf91465e39c64611ff24955ba408d3b5188105d04409aa191ba550eaac78e612e740e17ced587fa805d3b6c951e7f985b8716665f52bb3f38610af24c4f44ea9a757495101f0e43629299037d747570e4971fe3092f14f0cfbebf6c5e0b83b75118d1cee48d3b4a99d7c82e8f7ff3adf7fd0c53490802aa0b445f4d197fbfb5dd2e84779b53d4407cf834433f5d081b3994d52b728dda05f301c52fdb2a11f573021923fd903a49a97968e952b594183a789e342f1f1b15008d994b7511ee06c662be27ac9eb845197435d9eb927cef27a63b5263eedf18dd9d0bafdf6539e8484e07f213784aa5ba5366fc90c4a0eaa7f59437169ce59a59da3070851218a0325896508cdc8681511d1d1051536ef7162f3bc3aae53f16269031148d12e83b847bb47b1aa89d126d5f12bbb18e1e7103837bc99cf916ade9d05845a83ee787469e7e8aadd322c52c4214c95e59b0cea3b7e149fe3ab284f1d284ced9c8e0994b82c906cdea32726df70c618867074599797bb5af4af307d7b2ffc84e43bb66ec63d8ae72a57cb08286380aa6fa1bea478be3dfe39f786f451b16a292ee677a3c079df4594cb5971e1fb27af35cceaf4207b29f9704f18a8734c199636c9155d42cdbb78b050966892398d6fed3566698d8fee9558d2aa0d2b316de623468c7eb105a440a9bac6c774053e169829e50a0cc39f82be8f315f5a7298ac2aabe018082558e7ac040b143691f708d28e30d5cb457438d750e3ff7f1e6e94a03427e39295d024e35230d78defc9e44222e986a4a73f50c4a80ab3f9d379513c32f25a7b33c07fd65cc19c28ff6d769627e7e39a9b77f1b699b62a18068c0277049eeb721ecda839c93c6d09abf5c3ecffbb14f07327be60c8ee4785a8f5175b271cfb292764ed6386154cc6898e734eb0a2f227d002ab52337d7a45ba62bc29ef26e9a9e79400245f8e8ba9d7249b92a1ebd1c446493a6fb2c4a2d4ee34d63b63bc98e9217e32c996f17b6a0e8f13a009f8c84bf6f117292c1d8904af5e4cdb6aa649bba9a984a9eadb1ffe0690ec1f3ab51c85ebd9093071069be002bb3b46abe1b484f60ee969e8b884ecae2860c87a9cc883e113543f3cf76ea3395ed0eaea431f5199cf7f51c43e11b4a7d1223ebf8c4620daf668275eed5a8b6bfd33411cb70599ccc806af432dbe4f5e39adc5d27eafd96c0ef87e568e82a33f27158da8cb52e698f2bad08c1d14de790f650c844e1aa932c8dd53662c9c05cfbaca954b1f3d44287165d1f462371fa25dc6147e8848029b4c56a3a4f904d732707b417ab91310e0ca411fef8725e25014933a7a8df57a7c58e58d168c32c77782ca5beb03b2243f809d3d917d1b8800f257b3f59d69360606bd40916838d0de247b33bfd1f11612db942a733d7a21b3d145a7d95a7d2c7af8cf33ad7ab6a90a3a9feba3766c88a20e2147fbe12f542bb55bc722bfc83d22452eea784b1643b81daecefa97d0c89ca39f08fb6ce6d042e1bce2cc3839b7359f21a799d58e917f63cdd2b53cd64de7be870341a02daf184f6bcedfad8181eb51261fa99055b1fb258216e1685c9b15b1540a8380e64480b27179a524a12e4c91e3b611706c24fd9fde33965565563c459143e4cbd1d2a3b1badad888080326b4ac4529626ca8248207d699a47b4915cee9ed83db5673860be2dc2a68b7548b010d66c8f170e798f78a06bc060318538adc66da81f7a736a7ff2ba7a72f3a9f5c396e6748fa7b2f1f11423d06e14d33ad95ccd51eb9fc89b3abcd44255747baf279d2e0a39757726a88e348ceec53797982994df107644507d8306943c14b9484b2031fd8195d11e58fbab931bb7c3aafc27cb797b56279910bc57428d2a255e6ea27a9228a848b1f0f57c89f3d21a53665a2cd81ae0a9432cd9b26a7bae7924c933e7722e8da2cc886157f3e0b92796884638f656b5037df39ed7f4d4bfff529d082473bfc544a0f355c191ccffd91ba2ff5131a2631017fb6008c0954435edfe69f09506f1800c6bc03bcde2091eeab7e395bebcb4c60303347a628efbb55d3c26d59aba4027830eb4f110cd6544fd32a7d64a02a845cf56dbbdca465c6fd816826f12c3aa70e3715ca31a726f2a6ad469c2c4e8dcf989544ab938e481746a750f9f09a1a114c69279905eb434bdd92bc04a2d848ab50219028771aeb577d31348f40dd321e610da351db949817b65b7e6c12c69016dc038250115a6d8631dcbf7bedac7278fa507d18b5695f349dc40494dcf211bd50c42f36ead842d24ed3c436f6db0a5759df05a7b8f98bed9c03f02849b8ec9f37a60e5d672c30abb002462625600ff6f65cb6a116dc4edd0b8fcb560a9d8a3ee730bdd587829ce1b52bce19a9debf12c25a849732070119541ba49910074f86f154b80fe3ddb87f00020fb95760b3c96a1dc16a6cff659af5d22bbd3123b7127828cea306aeb03677c77c630fe3871ee45a7358037fbd687b3d296c2013696bd1c287eb1c83d5ae198db4d929bd1a1b24276f1a47a5c0bbc314e49790feb599c6e25a9e3e661b17dee7d8f3ecf9dd5d1ba175298616285696fc4a2dac3e42aad0df976322da90602d4f802a206bf83406e00e97755467464701415408560d74c242ca85248e83505c32b5706eea59d8f93382317eff1983ce71d558e8aef4649556b5cab2f21d74c717bef34043befda80f0db5d6419e8c4b735798f6cd109050c71812e36083e10ac8cd1de73c798d18868966e2436005b86b4b176cbcee045e5370df8f338d3382dc5e607e04532cc5b3051ccbc24cb32cbc1675b51e74429f013839f5c1aac1bafa47556aa6e3c79947ee06b06b52dc149085707f1a934a16f7ab1438ad30f2a95e9e7c7971b4a88923dfdb2af1bd57879e45a3bd660e705b59794db7e4ae5091df0cc469e4166bfd2eabb2a220a32a79dce59095fd4c579199c228ffe4e5f46b0ba008b099d9c006457c97768d28583a5e9a0e079ef82121ef8ff0605236b7925f778f69c4c646ae015da32a62e1d5b16b21eaa874d58d818327ad582c873f813859ed6fd8bb1d63ebd7f98287988115618ffee091b47274aa8e757ac66929f620e89951c6109d2d334ccf4c9de77b7983c3621e731acb07f13bf02b6baf44746cb613e70bcb89e380e0b0f7b0a014935d9083fd7d5e4a817b22b5c841db1c585583bdfcd073c57449ec9cfb6c58195bd9762e4cc864038e8661e8e98a2ec591569b542e238f71b0649d867ce487f28793162b38d3034eb27710a991578365911e9938c0566e4a43cee517b4eceec8ec152b5523d41e54572f8506a73ffebcd6d4df482cc5b65af64fc413cd6d136dd48ef4f188581edb80866c9732d4002a76406cec894e1f3b667e1e9182441c0077391623396b2dcb75b7c78cc99f2d7bbbdd138f533aa9391fb6f02f43b5f9448de0f35a93cb2d098c81b6a7e1f3f895bf632c1a0b3e7b9cdd75892dc95a805bda31b5aa13e747a8f0afc59cb562e635f0cb7c91074a6cb21bbd1c4fc71dc7f52062b13276ac556c824d5816e3ca64010325a53af0a6f38aee32845f49170194619cc5c0c03bf92027293ca45e48b78a55540c349a004813df1703e1e137bf744bdd9500490732c90bebff35205dac4215bb0849d6a837ae76420cdc58a830f2d4391b0a16105b798a10a9d8c8a2b74782883c767c91602553d402a4fa7ac082f34a49ce71493fb3deb35b766380cb1e4d0c70baa1e90a7bcc294c30d8fafd88135a05cb1e33a009b5c8fe2df482c50888d1b62db00011dd4cac1943986494140ade0ebe7952929a35584b3fa1f9e0b6adba01929121e6505976c58fd50c226dc8d4c7120806f0dcc0b7dfd4524493e26dab4c5e126d64eecdafa81514cedcccad8a5cd2e955a6bf103689b9adf07df0bc3a2a6d70f6b61e91fdfde0670fe60975339629156d7b7991251a13cc39a9af5819b590b7ca0923762a654e21ce5d6e6c26e65df40afb9947e927c5155e428d03be745dbd062a57d6ef3a63b5d9bb07d8cf445a7c8afcd79c4ed67cb2e50261a8c44a13bf7393396c54a3dc85e76a85abdd30028f31405704ff4b3fc80b8903e4623e1696b8bed4e898c6d8527172df697cb87041e7f517befb317c5c3039d824c24e9b4db9306b5beba1b854c2bb0e8b59d7ca4542fb1aa364be70ef719be4b19087dff28e4cd1ba51e22e1c30b8e71c3597624c9c61e651f034c0baca9449872c2f774e2d9f8bf0d75aff60c1d2991d934b440117f0cb0522a5e9c4377dfc1553f8b0b81ef1439f1a0d972bb87a09d7a12d7683a88592618406d418f467f1d20706fbb7bc2dbabdb063a9116704064e35e60d8f8ba80d8971989e7af714b09b68bf9650d39caac305db113231ce8a4c89d3a52ec5b93a7a1521d3bb8100c24073243bc86dc52eed035f2c2b8b0fcbf6e25da59a2ceebab4f33342a99ed77f40bcf8e42c32a4193b11b676961109d5ebcc9fd4df52aa9b16ad536a46982fbd3e1cb1b3353cd25285916fd8f5c5a04a910d957eadb3ba567a57f35e3bed29fcb2492178989b370c80c329b2000a3bd6aa39746817ecbd838ffc5edc2b77320acba8ee4f8c9e2beb4915c43f162a13247486c6495b01fc6588237c0c2b49ea514901188c1b414712ddfc48ff44fc8b643701e4bfa47d97000a5ddb400aa3bae1a997aa7c57995e06a7aa65c8c717ed067e9c52f35865fcad165977f83b65c8483cc09e8ca1a07148683811de3a756e024c6c7893706453609822e28cc16fbf03ab317ef91e8634d92fa4f482f63c9ad82412eff9a5a39f5900d4acd9ef09e234e354004d927def35c1fe9b2c86fd90b01262a43872a308b80b1fe92fee94c34f37e06486b3bb2b172ba49ce499f8183a8782699083ea1ff2776f5f7e896c46ed92814cb2b06aeafc2355655abfe20e5ce380a257fe619248d3dbe3e1254af49b23812c001374f24ab68985ed8e69f1bdb567c3d2d500913ac4f46ce6451af6b918d611bef5c68dfb7b490a158fde499b03eb1c8bc2f770b483d258f71e284d1a4ad48d55af77dfe4539d4fc7080fb883e371d63a89984de959a31e80a397764505f61ac7bdcc4479dc77f6331bea982a85fd3405442ad6b4775d7005c4ca1d0fec31e280c75c9deec166dd49e3a13f6320444c1a6ebbf8634036716e495a05b865211dda918292251838afbd7f494519df92a3226553e496568a645e103c8b7aaeb3466e1682fcfd8a831bd4cedab5afacec50c2da9e479a620016c681457b23d4e2e37f75fd14e1533d7353c9a18e444834f80a9e259b4c111f77beda4969881c6e65d98388c61e6a63926cc21d518285924725a9e52be4a7f1764df69bf579273af6ce19ec2553fa2f623f0c81ac3fb2d715e19197f83dc11790e49f0e5d0973d7f194b0ccc2886d91ddaaa88d2e0d2b150cc8736c22ad74fee892e42be04c4917cb31d0b3243bd080c372158d9674028422fffc7f81dac2944700b9856acb0b77b10c484b841916307065d6d73251ebdccef42cc449395cd060ba853951811a024acea4f708f28e6093830dd4a88d8903a769a94d71700542de587c45db63a9049d917124e80bf98af6e347b6427f67db9f50c3fd896cef7deead21334bc04afa4c02c8810261920082316ff8bb9fe8e3aca1b694104f65a1bd7ed8cacd1f385d1b7afc42eaf80f28e17198de8882ea1440eadc8797dbde4d899c712a3f89809cff32b6d9e27c9eb392679d62336a3b3b8012c3fb1e0e47a4ae74694623fc9486b26eca69645e239989f0c4297e9df5805c145ae2f738d61caf6a927a03c36e48b057a10cce5617a58dcc3e34054cd502be07a6763fe6609943c14564bb17962a89f6a81e5e385270edc2820cd64126f4b79c93281459443c95ae30cd86be37cb50616af47b1ae1f8354832e4d66084608082b5ab46f9327bcd81ddb843f689534febc96d368e25f4210d019a7d19dc7227041e692a99d6d5e4576bc5c08e7a9e854cd5f52e00e9487a624340b7e61f2ecc657ca26e7f439683f1555308ca2a311dfb19a31755ecb07230fb60e2c4f00d8fcddc6876a97b0c18e381961c1d1eac6887ef1340264416232dfd4c3142ddfe1837edc2d6b10e3da185b9e3737ba2d71c8c5b51e703e22b1d09baad4116bf51e380405aa32063756e65b0de67f6f260f1adf33216c9c45996bc0bca070ac9f8e3c673c1e6877bd629b0b2756249c13699fbd0d392a9d3b61e6809c90bb47fbcd01c66cb2c56f5a328368a1263914fa6d4688fdd37ad57430de08ba31440a4487d0bdd7aba0094d3b444016cfd2579b5995756fee850a996a64054360e4622e57f12eeb33a24eb3b3cd2089d4ffd8c32fa6d5d5e94fc26eb166480e51381bcbb5d25b24f33090850e1f41b08163fa35f6308e85f2bdd882e5a0ee9e8e5ea788ed72e0cd3149f6722a4eb29d9ec5b712be2a8e1fbe94b9e862ec522c4d8003a804b2351ca0af4ac5afad44679a59ba3e42f80d58364825a41efd7c6d6dfd73878c40b432c12e460692ba20dcecde364168ad7505aa1112eb5c861ab05e25163d519b8883a077d6d53e0cf39baa960b69b55c1efee29f8ef0df540637ad43e44b66dc531eb4c2a44bf4917b35bdf2857ab42d47ca623ef19d70f0bd92879031c10194fb7b221a1f60bfbef9c2027f40c1068db4f1988a06e749a78edf5d236a88c80047c6faed18dfdefca48554e32a4ccfe24dac2fa9554b20333024ad57c4a05ac2193c2672039236d0b539f8f36dd9e56ec0cce1b78474f9874d9cb6ad004ba7dcb486deebd11790a7b7faa70aaea696cec57264c04e78ed7919996fb8725b5bd27c1c8d6441c4a5f956bd4ab18ff1189b545a5eccd9971514a2f6364d1ad0a31b251425e98963a53fc8bc9f0bda45bf0f5facce812f58c5f46e8655d2f38d428c86a5c9cdf441c3ae1e7cadcb402b6e709f4e96f5747066fd7390d245125baa3e84e0d3b19851fef265b160490f80faca46726c6b379775da8691b73fd946fc48476b45bd043dcc60a95f909d0dcabe0f8981a6db1a5830d8084e8fe0b1b510f9612eff3603d45ed536ca586230381405423688dc7866adb6bbca814c88258991f4d04f664db168253442368ac5c83b1411999c5f1ca9b68766f14ef24268e92ca3d2ca7f34c15f72f2980b3f4611653c882335aad9d10120b62810e0c9e3c3e039b0e683df2c7e7b212fbf0861b8af1662a61eeca00ec174d5eac695ba006c800138f20b3b296da9a9ab4fc9b3d1070183a355bf3b8644fe3c9416cce855d15fb25dc8328e427a88ab81cfe56f5f2a12713f7a703756dc05c6191f21f4616e75d38d4009073c9a553bfe3bfbc88bc3ebc2915bc5cc8c946386039581fc8f1a36bfb428ecb6a3d0c09865529243f33a8315c1b053edf34d0bfd237fd0b96ff82122d4b3d7e676e8aa9b71597960cd1780bf671e0771961ebd059022801509fd2d67d25399278b638a9f8911cad23f7feb68637d08d0b2061e5966a9a8899eaf023fc9853202b1d1f71280ddc8676d22093b933fbc3a8ec22feec39d335f35b2d6fe5b48d5bf58497275fafe9dab3c84fad7f66ae7a2310659842e2c32624f67aa5cf7d524c8539748bd7121d0d214a6070a1beb395e8627a6795600e2a149848b557ef7c7a9cab9d579855d126c78f0da99db56c58450cfe50d7c88d4f7b19ba9e785185d3d1dd95fdd1c7044abc6d6fc2b9a42b8c64867906a8308826d9141c6c3d58fadecb5638cfc619af67c037aaf8f9926b29148959ad542362b5385a4b552f7e61f2093524a6d86d9a6867a053d03c27e86ea251f9e2be93f2a4b7341327b6f2c7b1cadf384b6458e87330c694feb975d7d777ba46d23f6fda94e0b4914ff51b9ab79f89bfe08b0e80c74ddcf1640e95a5fd3d3c62d6c635014c6b076917448a212207fbd45a50542b12d1922ad60ef87a8b01eeb94df921684dc3f2fbe8c222769b92f560e65bf43b31892bdaec6a26dc433303ad667f07f4013b7c74e31ce487d53d6138d5ad93d0c4ff7cf44072b6abf7a1280de91fb60ac8f343f292b6b034b25b28b24294b925643f1261cd95588c9160652112c6fd1db050b452d6737536239ec883272473f2ef71a235f17ff9d562d43fed50aa27574658a43c0691fae19496c337e01786e74bd8c58029a7fa012df6cec5775e12bc08b6d89e3057ced19dccc983c72bfce593dc129e12277a8281a1df381074424d74365024d995d256032aad2b49360dbe9087c8518b5d93bf75a2fadd0ab98afddb2fb8fb8207a389b67d48b67706ce1328bbe19f6603e650d858efe4ef77b7782b5ad78972f903f4c5bc1105fc4d31128724f7ed71feefe6982e2a24855a0ab2b40b5c1abaf6f45b89f4417737e65f7d337957a7c37c16f9c043f049873da8df427fbc60ca99abf4941a6ded871ee84ca193089e01a1fea1c3c4783ad919336d9063e356d8549c5e6efc4a9d1ad6df46f468cd47068d7b12ad4c6f87c4961732325467d5ce6f417eb7bf20f7ad89a865c539b76215437105ad9acd87426bdbc2a8d40b80300cc241dbfadf2e64b889f0097506924dbc05be4c2ebcec9cbc97d6b65e12d43b2c928f8db7cf38b45acdd6410d0be7351fafbb9ea9bf3b455ffc5fd24aaa1f0eb139e3fcf142b36192082a3607b06b06e6b08e587edee54903a87958df1da97d77130297ab5e70de2958344c0c6d1f5b4de4f25da5fa520cc04642d89b0a6c5915172d636f38d16f5bde372f3ccb14948069b518ffcedad239c72b543ed21c4745f6e46d3cebe75e5220c48ac82c07e68d9302533284969c03db72a0303a2f277ffd72f87f848139be5a9184d68f57e03d82b800a4b2450883d85c082f23e76daef11646a5084e92dd25248b798de966425cca2f1a61575e3c9b66973d7597117f72e11c5ab0f7ac40d3e000059386c48791e11d341e3ed5f79a04291cb151a74e76fff2cac40afd73d8682e8d876fe197375db1ab66b6af38714a92b6c0700dd194ac7fbf4c0dd701586d73fb4dc9ed4c200c159acf3549daf732e1e0b1b4f89afb573403cd88a8ddff96ad2fc98e974b7a1b9c0b0356d27335897156735f3815dbf90c7d8e60467aee3075e18dcbdddc0944784becb9f70e6453baadfd01882b5879f2987e15893a12ab12a623cec5ca19d0cecf16a097263fe53191a50853f2ea9fe05cab1626b27ed4568ecf8fc8521887d1f95b3e52227032eef943f3371e0e992f66186093483f07b112eb604af7ce3851fab2a7f8930214078d44c6d897fd9c15c9b3499238e8b676c9291017cce0692e466efe0d19e7d79d0c0b08e020c8307e1fec18d0a26afcbef88a65f2f3bf00418a5c322db8351eb5a94e430dfc903eb8f6cce082e09762669786e228fbc83479926e77a06dabd694122bba219b0b41362473bbd37badc877e7a4dd9b1888442c0a38a452eee7424891dc4d1ace19178991ba0d5f67b2e6ab9dda382ad77af12d3947d59b1ae7c8167cb0f58c8d0e062624d51273c2d89bab438b6c2dd689f16fbed4205be17d2346c7c74ee771d96588a50c9aafca4c914e6f28a9ee8d08d51c286812bef8d36969f2b29cc4bd7ce0c35791895948ffeb0ca2efdc4fa5e34d0fe9032cd19335f6229e6c997a247c34bf516ff6766f3362771cadedfd6015dc0641ee9b102f48a911f879a8b72d1d111dda7c0690e9189a7e48e371578803df00f2af4c6685ddd18ea877265a4d775dc31ae5ac1f7321d9900a07f893793b4b389bd0932a490c68d144136043e8af0b64e66c8e804a3463190b98405da74ad40c7ff66b3be7cadf1178b06446940e6a08584f24153e068ea37bed28051e848082f8e484f0cd762bf68d928b8c7ab75b51f3fe6940ff5a4eebc98cc0d99e25d6724810ba92ad241cffaa0a3ce4813de7e2abaa5b2e8d426afb0be22b52a88899968511f1006d377065f6b5df8a1914b858847a0817f71bb8244f1f41d178bbc969d81de5223d9eebd75b4848ee812e76fd51370169db3645db4bd273f6178009687ed26b5df36c8f0d7e8a2b348d26dfb72d940c3f13f332d86ed56ab35ecb5296e4b6a928cde93a2455da4d4e0b48795148ed747b2aa205fa3322cd4866d28bff551b4a27e4fc9c51c1f1e9498f43696cf0ee6384c2bf5ed16c1b5475cb56ca54ebd8264ebedab1d93de93b8e57368407ce4b44344c7df6d56564c7a57cafd7b7f7c64330afbc9b1174ba9daff66736bf38d5a9a5af296b4d45adce0eba4f0412153e5e05d511ef80639a390df865f0a61267ace71a05d8e32604dd348f139ffb7f7fe7111d0d39c365a6157e9238adb28cb2a61ac01990952744b6dcb4f198dbfad7384e09a18cab0b74e2b1013120f36e80376d86cef7d7ec353195359c4fbf7fc6e55d894f2bc1e61635f0ff841a8ff00f7b07b8046ed839611544fc8cf9a56440cabb027f866051bbaeb927650b6d3e919fb8a9004911a90a273a4e7db2f1d07bde2d03a6c7f850a26d703bb90aae61b6fd663ac88572f29097fe1fa83ac162996159c7f57be1926fa66c2dcbc98b7c5f38aca700275273d32021065b105c6f640d3bca8af1d36a471853c746552c4ee63769dc06f6c7f4fed3d5228a19406af0a94c4509aee99f517d8fcffe588ac07efd527b689a1fc61b7531ccaa0064eba894dd34bda6e2c1754925236a526a193701b355fc4833839b25bc39a91f22731f4fb2252f94d394229fff34f4c09425431e3f7fce326b081ef100a745b047cfbde37b60ad291893f180de25031d9f78ca7d467be58b6c22d5505b5127ce404acc2eaf7026188bf5a70c129b05bca29003aade65ac209eaf54e49698f2c2f3559b83663e8c9a2a183a7c8e1589d29d32abae85586f7c34969bc8e4826ab498b0b3009bd04941f2e8521aeb820bfd95dbf5606f15e0f56eaf62bdaf09cc63f7462a611fb21c9e0193ea223fe37a092b9433a47db224f63d2d6e638273d8a3a5a5b25e0443f7b73862a8752b967c4ffe95807f7eb67429ecb2b3913fe996e1770a7314b96e22c0b188c47fdb1565e5416709cd534a22bc7090b7a47215d8fa362eb3723c0ab8fda6e4134990bc78b6eac19f2a10e3663f8f5fc45d3a58e91bd752353f70c6162a57d743a54181c3c1712f209d19e990063348657a9d7596048a4158602d95d5628a2979211a1f682c0f3c9e2976d4922731053eddd72d02b2a4865d9372c0b1f2d07bc3149b93d097fe754b33f8fe33e12982fafebc48e8525e15dd5f20dcfcd8d2bdd73e98c5096acac97a371f652ec27f638984c6fa568b72612290884b8f2eb866c6af003111c0e74a61bfde16271cee1804f7ae44e36d9a1a785639888f2613ea5ba8dfda3793386939787e60c22e4e5ba465732cc13e9669566373463ff69c384f9a30aec5acacf06df3112dde33e7a0aa242c2ecdc6371bbf84195ee787af4ec49105c2d54a957247a83153ab51e5b4b0c05c707fa982b32e45a096ea133fca0e6ef78c7c05794fba0436c762663e51ccdddb8ddb43b92d9115725e0739e24469b28c99642f2b5faf1291b9db856835ac0ae64529dade3be87461d804554c01dd61f13f6e8c5c57ba93ccc12425d8f6bc4ef1066052743f5fd08200a7d035d4ec76a3fc594c140f879101da0d01fa77fce67186a3a5f422fc8faf92187443c0fbda2bd66923f3fe40dce5a2d7f243321ac50daeda3f4112f4543910ab6c1dc1d013839752544a5f1cee751c874f8a568ba7a7b56ee674e17257aea923b6c534b9d5126937584dd2aa902462f6552983f0aa2ea8dc915c4ff0106769c3a2ebddc31b4c45b38ad661bc6d2111a3568280b1f85db8674bcfabc8c95085913a306d41d3583a4bcb98f37fe7e5f6241a42b879ffda7bdd6847f14070ac86806f8751c49f9afb1915859891fba2a2538cd9d0fafdc927e9bf1b94a085c70076a2ffa0fa3ae64bc09a950f60ac3c8272517f3b4234d913803e9efefdf26460815b953a03e7302e08d5b507f5afda042c609fcd69a73a9909be04686770e1b15d4fe77be7998fe00376adb36aceaa74fff3832104194901743fd34effe7baf9250b41495c81b84c1196aa52caa71acfa8f11897302cef28465fd6b49ed2a137889d6965313c455c36f7c627375b5e0befffbfbe6b50f80e87767b06ec511a3a0e69232921442b2cf56c8693d09ac7156a99f57ce6a3a91956c5f840a7a376368d903440f205b78d216242ac7e109175bdb52c5a70fb2a010554963628920375da97a196322a87e69e7fa313e5e38499b6445273705d32d0e0c4b719fe215fc05454a697bed5c357df8308bd7b704aba479fb3d9275aa04e7f5c1d7989c5b7d249f9a05ca30b61e4dd407e478743479e8320b14580d2a7277133a18fd92a0f7343d6a9a2a0a8d81d83e2ca01b56f24f024a158a4391966b3a33180f8bceecac5e8851db054a39ef3236d82fdceebe84deae5e80068ee1b77e2e483d6dd4c5b0b3176bf377289957a4de9f716c2b287430c874c6237f143d448a9eee592cb80bcbbc0072ecb085302f790ff7029da3f2a157413adec88c4bbdd04ce1865abf8cc2967bbb9ce00e298ea44607cac4bd70793fe5a70ec36d842c6f23c53c655db9f78990ea99e2ead14394ee1ad3a0233e23c94aedec692bc85e8ba09e2a9120ac55ef14e72cbcaa8dd98dfb7eec88024f28515d29fc37d2bb4d24ccdd555817803775a452332bddec1e47277fd750a2f8a95c5e55189474c0b7c51cf5f0ec04f558e76abf1b0fdd6cc3be56dd790ed0958055ba3db5fb6ea1722be868dffc5a62307f60ba3f12c8c141bd61da7a943b0d9aad24cde0336139c726f1e0427c5da271464a8073013d69564355dff2a3d15ceb5f92bd49f85650f221f8b870c4081eb5c72eca9c85e21ad857fb83fdde3b4dec0af9c9e4848197c2307ee57512664c0811316db7f70de99fcb85e0123aa3fb1ef44c0fd1621d2c658744d61fd53f067d38183017330da24c969cf293b5b1920bfaa974dba34a5c2391ed71c647e9db6d2230ebb1911006d8dd9b440031585aaee67456f52e22990ed1c08054e3734ef7ec5de90463c076c4376187a64e827171e1e40618cb411134369ab8855f07bb492b96bbbea66fa76e6ae7618791233497add9202bcf00700179ef1e6da2b653355b24355ffaa3ce039583e8674f00fb86f05a9b3b10d85cb28748506cd72b78e1f86ad3c6449dd222b778a72a5273b7734f7a3c53dd5b089c41fe1169505b2ec29254129eb048902368bcb0fb6e020d01d7d8f18c50cec81b764be8cbd35499659714f468cb8e4fefc16697a00cd775e992e7832d77ef94cdbb38673c9fece98da22c5b1815a6416638572026b38de0535dd49689748ef6d10772f2f710f9c167ff4b5624919ca9a99935488c6420d92fc3528b5bd6491b62dc903b0e6644546e3273b54a96e7805da5afaf354c4817208dc4b88642f7ea33b9f9ea227a63d56c1920372eb92ae0a483bae7a6b90b6aa8631f839e081923e16d80746ed96a4b19682034eb6e51cfd023e07aacb04b41d2cd849ae3188d2adf1c95715d6180e76a06fc17f3904288be11e52f3d30a1dec668432e23d0d6ac087d49722b9b499cbe0ab14bfbaa54d673b29fb327899a9ac8401b2b7f12fe4f3f1704534912979d32cd9a381ebb4450f35e8720dae7c18b0037a19cbbbb31aac8d0d3687bf0eca9368e4c34211f345ca1d3d5de55484e5e1c9ae6a08f50526fb152952996ccfc5599409dc60c825e85cc00149314edd12ab70b6c32a52fad11cce708dfb16ea973fac19d04e93e6b07997b65740e9d3a234238920df770d46056952dc76ee8d8c258eccc40ad02f379a17f67cb9066cbaa5e77e23d3dd916e7574d7722ef4d5f0db495d04bb02f4b90b5ea244e725b106a1a240dba5e778c074fd14f506efc0cbf115421145e5e467d42bcfe4023039bb4127990aa80e1fa8b5193c5ba3c801c6e76a6f5c50f117eca9e47595a89cec0c06e895efafd95fd82ebc0f51703ade06222befd640039924036ed21beff9062be928848a3e4bcc93faa527e855ebb32b570c0f07f802ea77085264d6404fe1eec4dc151fbdc1bbbe2ba050fb1d4603825c2edb25afd4d4f621f75fe09d14accf3c1825e7e4cd10fece9c6a8b2547b01bfb4253bd4d6fb3c0aec230fa4ada1edac7d41031eb10b747caa3b426530c66564515242edfe672813489fae24952c318a0ce26017c448785990e28bcab5dec99b1ad988072cc8cc39e1531d36479eab1ca52c9f0ec7b2eb4e18ac35954a5f872fff82f0152957d9d80e61d442d87fbb2a035d919418a7592eeac6c6382546cba36afd1d9dfaa14fa6d4ae7193ef6953efc658adc7591f0c2840c966e4c21aab072602a15ef78661109117b33f7cc36c4fb05401a07d9765a16487103ebf84b2fe24226aaa388f69843061514511b563721296c7a6c6a951dc296678d71ac3a44cc23e1b45ff541eba6db0e4f26032318ab5c174741aa029f340b9a69e15abaf95f879a52557102040eb9882ca4dea37dbfab33f5e9704aae29700810794b1dafc74abecd7158082288ba9677411db1c01da7fa7d1096e714954cab886afe64c7b7b4db74fb79abbb612930c1cc36661911e064b25008d54e68dcd6d5aa2d999192199cc1c938b0e01223c8db209a386a57b31dd5c6203770723c6b623af146b03771fd3b6d3dc618b395c8614e44172c671f51162cf40cd4f7ce17d5d1ca88766a703e44989cd0c17d2ca25544722ec19506a8108722643fcc2a77c7542632d406dd3c94e4f337cc0bfd7708fb3d2bc5fed6da7379e1bc29425cbdf83987460b490e1e2cedd455a52bf9a183ec4ad41e9534ee3c7c42cf7d6b7ce8388ab4180b7ac35e085a77823ac297219f1ea92701f6d2b1f9b1851e6495f15161f951dd18847e5bdcf3419c0a7f69d2df354889bffa668a980fce82cfecd3a22b7d590e03431dfc59dc7340270498e3677c5bc28dbc2bc35886dc79775a9a544d9b063d65a2c030041d7fbf6d52f9d69744e06490ea26b7251757f9cc348fbe8c448e86ebcc1b31053fcd90c3fd3933c99eb5187a4b0a507f084dd61a7c6bffc4e9b78cada22f8430e6df6381eba2e82da7db2e5394d479da7600bbdd28eef79937f80f05cf8417c5d76afb73ec13122beecb2c37bd3ac6fa1e66fb58fe6671ac26c69df49bd838f8573f716e157cef6d13e7bd93e532d55201e109d6ec85a659fd30940c322dae4768c5ce9cb96041a4af8c55e1078ec47c01b0308e77e489b309a148da9f5f2c98f5f467568c3084304b744b847ad52c7169819a5d78d40ea833d37b0091f57b6139abcee2490675e82f45673c0051026f1c0b59f3ca70a06f80733c4a408186e7fda9c9de56e34dd0f7a3dd9a0a623e70a0ab7e265b4b331fc60451a7e5088286169793298294b8d998a3bd7fd7ff4deab83e8dd9bdcce7be192384a007716ced315cec13a6f2cf201abdab1b59144da2c82fca62e1ce70ee6f79004617a61feee236230a909fbd1c87a111af541131e7ead3051678bf8dc0488494a10067087351a0495300672bab37e8efb8b9a7a167ca966455d2542b61a00ef39b3bad119c3171403c86fd62665543fde6b749721d4f3ba6366a0073c8cc7385a50b760b77a162cb3de9e6f3112ec5ad2f41adfb00cd4ae0205dcab7cd52471eb60e35f5858662249fc487e7ccb36447607f130eb7a2efd47becaad3ebcc4c09fc6489955bdde1eed4e9b01fb081c3e32207631c09a2e6e7fe4be273be1512a04680a45f6bf25dc402aaad2def9b02c54a4eb2cb767527c58e67cabc0cd5e35fdcea8edae84e6f153b9a2e1365859113fdf4b8f98264fbebcf3dc04d273db2345e2e173f83fba7e3ce3a69645fbda0077cccab7b8b50d321ab53bebf81bba100120930ff737b19784bd954d1e6861b2aa5288ce7c02ef14c46e314b306b93e4ce59f526d94723b11ae88faff71e85654ece60c79077369eaa6aa3aeadce241c35fdbcd243fd35a120b8f79b1390db37251f3277e1a69bba3e3fc2df057efc56f0a658f19c50b58951aff3a9aa405b9807c052c3095e7b1565167ddaa4f663f3dd713cef63e789313b67ca349823f87ef29d5e85a6c86e5fccf525da00e52cd0aa937e1b120d39ed65727eab0d60e500259c35cd04dac91ff5747fed8815a897a9a189ce67b8b34d8e901b568e3a82dd25f3a42bbc8816f632c60071fdff3618e3e671d849e45ed1406b11d80d23076cff2bd7a2ef35f3d393b4b54e9b537c8f62d2a9f603418e5145e78d8fca87b37447c6b860ca768c665f6d4776b6d645a862a75c94bf70b7a130b7972e149d3db5f9db4b94fc7adb35fd9de0530e2b5b81b94dc5437150d4a45516e0fc5c01c36006e0fa175f530a30fe56020e93b7dd00ce2877c127018539c3d95f3fa68979fe2c6f167e397d3a45a12f75b1a46a9a2553963dbc3169c1e42fa2d6dac1d8523d9249bf18515360f65965b5e3a7e008c8132a7f3c7a95b5474546093b2c60a70e9de7d54bf3978786733c5aec1a4f5401b2aed9bdd21adaa6ba80c23f5b67cffc9ddd2abcb87de5ae8a89ec5f51557569b04ecf13cef16b741db375eac88ebf884ddc486cd237572ccec7bfdec3f1ca0f8bdff413f8fecc9c5748b1b3c27b5c812864e95c617a4ed403e5563e9dc09099562ef73e0a9c27b78d8edb716ce3a57e1829e9db7514babfb6cc38b94fde5e46c748feb68010d4a72fdd9490b6297ba48dfc648e5d311ea555879cb5e7feef4e4789b3f3ce7e2f7eb0828ba8b7cc7ae70891a5ec64b75a86d5aa5f449f02bc4ac108c40493662c92139e4fe396439cf93717b8b9767596a2f86488408cf7d6c2547ae4d36226e3d3fd857def45c3e05be1f6e4d3d9092ea0483c3aef5e6543bce6739577a04ca0cc31eeb96f1953c0ef04461d3a65943fca5439e40b7cc1d6d395fb90363fbbf3528656e2e1305d20e870722ecc4eef1fa9cf77b4adf2c23cf6b12002dbd130758e1ff12d3cf751a97c991e30f88472ddb3e3da1ab236301059702f3a730ff43a51d94fb37dcaaf066b64b5718729460b2eeb5dfadc89b33730b6eba02157dffa90ec25b68ffc76b7ead7938a82868d1309112245a986395e0a18a84f43c4f9f7d9a68dbb6332ec0eaf5b1bb5e1318ec77928878a1e4d28ded47d9475a84b4ed6d25b486675a6103b5c8628c7a01a6fff5bfddb7c5af281ad98be11e2c9b3692b08aa0ddf73ff856b31517f7a588f565a564a59d7fca3e0ecc245b0911079581225cc9f716585756c1a24c265cc3fbe0be1d6009574b5a7fe31f46176f051ee86fac0ce22a9e6748b95ac3619b8779b59d0160f5f2e6835c7ba026101e1b638cad95972c08197ec0b3d0907b29a4cb526285fe23d5a246cfba16c5077c09fd0af01e52ec5e396d78f5556efa7479f558a1878a4157ebedb1bd8fd92a2b4be6f3ee585db07d027578be73543572477bed6473c2d793b9713c2af95c1b3be90d2a1fa970dc0f2e0156d3eb956c33d0c52362b682a782c596e2205e3f708fbfaab0b818b9a98c7adee6ee8b467f875203c14545d7700cd4cd32c8d951daef5ffcf07aabc2d32e53a42db5f027be6a834fcfa436725d883aaef476973579f71bbeb8167b5c301956d3a713a44645b9f5eebbc4d76247b6f6625336a28e140fb8364301ad55c900e220b782629992e3774e25bdf691a99efa551068bdf24fb6212e1a5de87c3564bfb7a1530cbb74d9a738afa3a631f066674e31773e7289d64ba21a9a7274cd489d14dae2aed62db767ed2df86776d9db7e6df38262a335f41c9c0d7c51ce7581f068f21a9b8c3e4528499e0d6e85c0e99b46055b54b6eb852a8ffcfd52ef73a30618d103b2f60029cc79aaf675bd259eae20a2b29dbf244db746f8877c433c03dabb5faf4ddb84944720f5294518581f7e2b2efe09bd4ee5df79849dea7718e5042325226cc33ffebaf6f6d8f82409df67f3a08dddff50754fa0a6c8822af6e21c8a4af34db85b7a358e9d82a04df8d725ba64f492066f7d2888bad296189fe8e1f82b982c76b8d280c257d4846760c66d6d8ee5f76af8c505059c43d9524062dea45bb69fa3bd29532ee7beef0d235370cf6a306a35a132e0123d90a9f53f2cffdfdc26e4240afb054ef6ef1373290a46b489addbde71fcd795e54b5f57ba0a248f4dc9a5cdb36d8e5239c20d817cf7bac23ed8682f0a437700faedf2464a38de8085acb7fcd47facf302220501d2839003cb2ae59b085169a6618d9ecb990c9bb50e43f7ae6bc21663dc3ba38d7a3b5cf9ad4764f2e939377914edac116fa5344aa1a5c98a86aea6aa5020c7c963ab86546d51718519cd5bde42a5dc577727a43c7d10f3852c1fbcaaf162be4bd7b0db19b27d8aa5c001edb57cc7f807d39ee1198a8539173d37f03af363fad5d0d5d3e784e3065547103fff0360f7cf3baf0b5c86f0406b14ac2650b5481a87c26ebdd3f8003d1aca197f55e2faae3ec276137ec2c06a37e59fdd9c87393a41bc44e838e386cf9c8185b5580d6ed6df0d5624e23cbe17a540ccae1572dc761c31df7d595d10a8cfb1cf0e198e0a3161449ddb22f748ebaa454202e8930453ed596d1e2eab256af2e7237b480f8c75845ea18f149c24c5f1fd019ab3666b3be4a458ee13183884e0e42126253aea81d11ad1323c0318e7cbbf16532411d79bee1b20eedc599f763b472f40a4d252e8aba43f55a3ea4a5d1c95c8beab3f0b7656c98c0365fb8638b7cf07e621f796901f05a0c91c466f00f3ec0f4971f37b83eb2ac7703ddc589253f42ed04e0e1441d8aea4b94ad081fb79fbbea4ed140de1615453407b6fb3ea7e0b0ee9a64a57eb3e835d2b85c8b4365a8a534e41fbf01cf5455fb09a70e443cc99d49c37051516c9c33ec0df7096eeca6fab4c85a167236d2ca661d205fe7126698707db7b2e7ad72b1bac9ef318a2802af67a7ba6a2af063b54d4f44d8738c846ed2d9c5fe4f867775c291ebda54bb876600a1f7699ddfa65da4329cc99fd184493a12f061b7502c08d56ebdb5c958925e72c5270263ceac527240adc55fd79f824a40afaa93b4a178d3b9b4f353475f7b4d2752d4858370f42bc716203ac3f6c0646c47df99cfacacd0a4f4d7ecd31d7239069273323feb00070a90bc8e49005a0c5d03c7ccf04074e6e99d80f991c70ac58670d8a6431aa03e16b86351798787c6ae25d0c9f48641af83c8ab9e7665494bb5edd48d9944007d3c51528644f2122702caaac277818f9b27fdf18432e7557e53494453614ffa54196f03493856307ff32fcf75df4d3e1c679aa1f6f8ccab14cc9f89d29685bae6be0b3aa7d3923c6e4a71699a523f62e42ab62f5bf6f7636d1af1506c924c032bd409fe63b04dede990bedacb91935e69c2845e309ed3c46747881372a04fc90df481f2895f976004d81b1beb1f42e2beda7c1706f6204de60c0143f836ef82423fffb43735f83073fd96938ad74e3ed896f33242f8071721e878d765a788885316dc6f0b9742c10f39a871c4bc8b979d5b8d335c70c32e862f959da7a87aa9c7f3eaaf3e6c63f9b8699901aa3d62443b049dd373c95dc4599c5576f16ff13d329533bdf044b1472d91b8e27889c08cf99a803b590154281e2d330390353849180d49ebd2f2bf56e1106364f75c4f1ba4034aced330889edfe7b4328a1d5f13d31a3cafb5816c35e6e2e776fae2cfad000b23f649bd72c7d7455a158a80fe732f5854ff34faa17825b0742c4f09317c14fb7c2e31015c113b9acd9ab5e0d36ef3652e6e84e189c709e27c57b9f28c7327894b7a4e569f4a4cc2dcc0142df7f52d5eb135699f273195c69470f2a8aa252085e20f563020328566c027e6e784b9c078e32487de31a1fb36c17c004c3c2ac6ff76016b6398be2118cfa91580acd56fdcd0a94030a3b8af72baf46e53a4b4347387e63b51d39bd0bf6cb6ff21bf11f6302ad336e9138c18fc80fd04eab011b7f000742db1544be2edb6d8655295465028989c9078e8135764be5cadf42f330924dbd294afb4a9f8ef7d5ca808fc0950abf16242086592ab3954db704a6e3c3e90c6e83a2327ff8f6f0ff1a63402ad5598fc192884f1150a9db05cd6ae56de09e016b0e21be7770e76b68097b3ae14a9b451dab68220ffbe98756524334674ff819a05ff510c808c9b6a07414392af46a3b49863c3a3e2fe137dcd66d60489035be2532e51b53a4418bc2f69c19360f886e1ad5cb7f4b3f6aa2cd02449dc918b3581bdfdfbb44b1fc2011a1a0c1a64b5b89791dd233c2f9d365e2fc0e044f460cd0ebbded1bc1cc41be73d8a7b222100a6de8708f2308127a468d8847eb6bf0a7c69940656b5ad373f3aaa3e43c636d1d579bd42df390ff84eee1d60dd6b7e898da9b44cb3fa228f6587a4ca112f1079261dd9aae67026f7389a8abf86cce5c6450c5f1e762c5ec821595f0218b8cf7d36de2a9cb8b62eea49d1c25f2d046460a4d509df9cc0c30911e978ce901761b88fb91d6b3aaf57f9e41e69ad7dd326f8548f79bcba5d8d1fc1e4ad2c4657a53fda70ffee613c800912d2aa1b37eafab1c8712d448bb43949f1b016a85d07493d86c0c7440337d66a6e0c6523e4a0a5fa5ac46e73b4efd947a9c2b111975768842a6ef5c713fa1838eb8f130b55c42b93a047036a509bce4193f1102f489adb02ed7bb17c0f14c12232ba6f485952d4c09a091d307ead9531aa17fb4a878914b7e960628da2a5511b6110d3d0f6cb2fa9172f4ae51db57c58fea22aa2b88703168675ba1931fb009e74cda6dad618d27e2d27691e3160c310d4a6e209d965e35878a220bbb253729e78ed20583edea53af84d100aef92428e4c4fc4828f388be047edd46bd15db1904fe91f68f540fc4819a81b1ee8c934d4844868b2122d092b5e7728b54506548009d645f856f60e47fab816c4b6b9ed520facf6df9c169ac1047e525d5cd2ba385a262ace5971a00727cbb8450e917b9fd3d3c9eb260f8e9bc8866ca9ee24ceaef6e1e1b76b2d675c72ba925a1acb4e5d1c252b99e867d41d48d66749e473917e7341534162beb2f087fc0e7921b60bb2df0901a31ef91cc33f1638f126235ec03cebdbfb668995ee63dddc4f67670a525a667991a2eacffa330a4daafb74bbb15c538da8ef083d0394e4e58756139f9bba4dea5435fc8d007aac5fb78fa0808c4a2753904449f6f9fe7b4969114113fb36ee436fa2f7d3545aa5218cc44f0932746c3a3b786a04975cc5ac316874ef9d423c9a90f7dba942d84f8c4d063bc132545bf015d7a424400be68dc97a0809607bfaafd95cfe52549b8b1e2a80bed731291826f5c2bcb64d280f667c30a31b7b13da8033ed9116c3e715ae822be40b349aed7989d8a69123bfef260e78b29dbf9a05d07b1de33641379a78a0962a643bdf21351b16cbabf73a6f2e115654eb9a069d75e6ef696463ef5175cd36fec8de9c52abf4df33bb0dd9de722c60a1983dfe161fd80d80d6b1566e734300eeace4130d39c6cac0828aef953bc36a42f2690f819f7316785e0a023ff89db54e220420086008c05bcbdd55b97c2d4b7cd43cbd9a650bd5aaa66aa027e4bc86182f489dc880a622c6e51fbfaecc4530ea2fc9dd56f3c717cfe35591c4fe5750c0e3c787d613aeafa9272dc35059ba88d35e50330fc4688fb39298adf1a7baffaa769135877c8adc1a4dd72f2827ce314147580359c2bbdf01751421b954f482062f4a58ab9aeffdbce2ef1e3c1465b0fed11a247e8f3fbd68c166119210721d21b0f1555a723149e841b5e51d5974e23d3cb85825d866a2ebbe2f751713a792bca7a414ad8054c880524a7cffc05a6354418856ea79b80997af6baad924c0266741d1338cf74fea2cbc913599be91d775462c9a91bcb26dc1e19de0dc25e8aac165af437caeaa611e15c0baf4f3eb6331add712881ca3cf1349e4bbd532f940d082f7276f222b63550e0aa87755e88c32b531ac0abb534e16343d1a9afabc7c9393a645719a475d240561e8ad05bd157e74e42b3dbf5485645d90048c785afc449dfdfc6390d8fc4c35fe443a7bbd85842958f1cc3c8130a896aeace1ed4fe651d4fdeb0a29640db22fbb11b6aab4e0141f372c80f9b304aeb6c860e0d203793bc0e2aa9b685e69a1ddae7852b4e1b3fe4f0c6bfe44422e6dcd7140a83da4b742b7523454191b84ed60822fd53b9ad8bc2e3b8d2d4177c1cf21a6018d618fc7e22a9583915be3d9f10b91857a6f371b3be0b8870603c13e179c3a3dce05d42ffb1fa15aad27a5455c9ced6cafbb13456e433d55d50be5c4ed695baf0b17f33e1fb53f812acbe50ae46f2c74831e1daa358551e8e574f06ef1dedd55a872c449c27483c2e1f2963966c4aaa0993c6e6b7879f649fdabaae7925a14199aa141d7eb5ef6ecc4e67c21b959b5a2dc3c4d7a77119185cf614c1582099e6fc522efe062934e8edf28126b47cdd68aadf824a2d23e66a95235389a967944b0476be86a09c93b1f76141ac0097d6f9d50d22bd09834ca85bc0d26f79b584757ffd25ccbe5f2b8b3cc79d867992550c133ce2b5c81d04f08af0fa2b6134e049124487da35ba1bc85987b37ee0f33e249a0010b8114bf999838ac27743fd9fd5a6c16ba18b924b190f5c672ff2675e80cf2ceae990381435cb96dfb387473fd27ccad96151f52908527ab93f1499fab2e668250fa42bcb9999a44245259b8a79b5a11dee3988de3d4a3c262e45a78ef28323a9f06b352298c65ac3918ca4f04182d1ce00a43c69952a2e9da797281fb974bfff24650a85b88dd10e99569ef8d6b442faaf9333435be8bf026f554991c31373d370727ece14fb3b5aeeed141c1acfc043de3bf9325d6848b3200255e3a9e9be964b194e7366e18927776e814831f23bcb18ab0daed4fc4940c27d768483eb97cedd71b5cfe7deb2aac56e602003c8aced75255b77d29ea4bf8a0dfd41510b6dc49123d7b1a9d3bf99c0def2a99a3d485ec4b963363bcabe0adbd3f5c2d7e06570c89816596bc708af38a3d3d0ba98931969ef9c57f4fd5b57e442203603ad665a1e399aff4d8025b06e2ab96dda0ea0882fe99e8e9bda558316c4f29592140809d3dabbfd359eaa569d0b40ae9a78cc79e9e02c4c2adbee11aaf24f8ce03f1343eab8b56a31c861896d41a5bf2ac86fce3b53f14115c86592bf4e5617435acf0fc4df680da5298db1c538254bfdb369165c350f0298c1188fde6f2bf46b461e91792e8573ecccc67b68cb2495de1f5a9dc4c8df4ed636bc465a6953ace53b9c94b0b99c212392113e8c7a2a9521160b4fa55d3bd87258672d69f8221e73841d1506a2dd2cb01e3eb25c8af827dfc3fa49311dad56ccf4840397ff378cc6ca30863545a7b0f20d16c3ca8a2d8a4e46b3a7a650eeb632f67e2bbe3f791add1e414613b0115b3317adc79f3c8109a280eba1a74ee9dd12c84a0a459b784098fea08722c2f8a5def457a70f440e59c80f58a8b6d959621896863669401a272d910ceb31ab502384d82a7c9da299f6804bcc5d3655420648157e9dc1473b959b7eb50cc48e5b0e5d23d354317ef592638ae1270585803bdf34dcd194208671e7a5ea23bb208b6c739f01d58ed9ae801244a27607c21e4e78b1dd1e2d5dbfeb92b6097dc9fdd23066eb22a76b7f712e983d5f92a343fe6597bbe1ab9068cc2e7e0b6f4eec6c663c61071088bb3dc07ed06afdbb22a825d8bc06b5a45b4126fd838f3b8145625a1f6eeeab860e15a44897d6efa3b2975e25437ab322ad2bfe4cbbeee4032ca345c8e01eefe3d05c77b4b01becd52667e866c2bcb3fe77282d91158ccf2e521f12202a457fd3a75e7cec99abb553b12130af998a59641d360f58e57e478e4afaab391863344cb0f10edf368cff2f2384210919ea2554c53a210206056600c807842ad3a7a636b1c0c433a8666273cef1e6e0e7726c1c035058d21ec3e5280c9d7d124e6ac87e591f4572351f055458b481f881cb326a2f1c9abf6cc371f78de3297205253632cafa2e0d2b7467b943081b2b8729b45cfa71b2671a40bec18f1b3c345f73395a721bf755d9154ab0417d709944397b410af072f9a9246463892c0712a1fa530a946cdbeae1dcb18b22a7800955c822a19149f8c5350a692cf0c740afdb8921bab25dcd1bb2e097b3d1328ead62d4a185daca634a6a3e75628c395988277fe4104da82ada64d2f1b17ea6cada1c1aeeeeab9b4ce10de44a8ba4c9f33e7a5bb5785ef8b10ee64498e31763c8c70654e3b4529142be804a956c20e68d9dc1dabec1e9257a8e2a0eef16a2a2fd2fcce7efdf191bb320eba2c3c7999eb8c3b1f38691f5e762e97f7bccdca4c4d7990bd00992687b03bb5450040668c992c218dd7d910b4c2d8c1206eb9590cba4e0e34e310301e2fda3b9c161b83748753fe7c5438a593fc15edd36337ee491cd4507e25018a47d3ee25d186df173d010032cbeaea4fed29c41df4c7fc25b1766618b86d9a9fcd6c18cb5422d1196d9700844edfd528fc768454cff7cc3939798ddb21ebb5b3fba1076c164909dbd9e9efe28d6b99b70f770df3003779e8afd33123fb0f9be00ad3c20e15e3ae5e64578faa803fbc4fc798fdf543dd091a0fe4e190f692b37691891b6f7b12423fb21eeb0ee08caae20c8a5ab67887bb1c27c100fa289c04e02b5683eb14e478d9d05c85e3d23ea2e1ed5dddb3e5aec382c3eb369b728ec445c135ed4cc81bffb46cde1bdf568e4a265e7f73e44a3c2f3abbdda54bc72b84ad1b6a4c98155999e4431cec1f20890e95cc4ac9226322f77d20f6111d5df95272b813126febbf8a9b2c7e1bac335a71ba9ebcea08fa722815b23c379d9221488b2f1d1499256067e29a72a76f3cbcb260855c6039f5e6a4d35920ca075ac1284a73ed6cb8851cc241264c34d7028f3e9dda239288bb7ed8fc75076342ae93aa06befda1cefabe16e068839afe8411af31b09507bdc8b8eae652cb380dc0b48ff6096d39f929e2608d1b5cb33ec87fe727728b933bd48afe06c1481deba6303aa22ba3d4f3ecdf958daa64d8934b9dcbccff65b8755871a03aaaf8f4f1252514ea47e2d805f57c526e7c0129a77285178141020bc1e87011e60142256cb288368af117917195efec90c205f4eac672496a07e9aaa97e83faf58b17d75de0e7640edcf59ea1569ad3105a7e274fd1e30b2ce7b7c4c5b9d5d63d676ef78a11104dc9d309420eaf820ccdb1a8b4ea68dc680dd0a15cefb9c605247cb94413665a12dca45f40ea4d75cada0fe7eed477da523915221685587744e1ad1f24968d78b84ab06a8ab48e0b2f5a48822043a7a0223762fbd27dab80a9634840e8aaccd1d89fb3d85ca522fb7a0c4a3869307001328c549699a0c82eb428a9edd710c746af516308a2273df47e830d97d02652634cb60f3ce84d9727e16182ccbe0b35450114d085d20a691ae3c323a36817a712e9067b1b5b234f1e7a925693fc41f4ae9801baceb0d96a61aa9eab96b8e9939a27788fb8d0814bdd10058691f3510653d3e3006bf20922d85fb7276b0c167902b10b2e567077ebf59a9fbc390fb107778aa16a6cbae795f9438d0944d5776b4ec5dfab9330181905be527c0a16add43249beb63f928d4f032627a0b3c80dfeb2ca07e80deae2fe00e2c0b7b274ca3ea3e469896b97a28bbb1e6073c9e9b758058669739633ec48270eade38d645d0187c2bd3e288d0e6c798d233a74f54ea12e14f7015d58329a95ace5b06cac3041c316948ffacb6dd396be2fffbc8ecb984406bb22e5a2ad5509436afa67e249b1a56b83cf99073d64e99bdb7f85fe1606384bb5fb9a5afcb53e735f6d33507e723409692de0cf8e47ceeb7a43a0e82bb275d4819e3f84e1291b50071a8171b6262059fe19504d8684d38fbc316402cafb23d0d855701fdc03f756295fac762fea9917bad6a2ed5efa076d00c477ffc1b9e150599e9d755569fdcc3348330d810a01ee6041bcd38f222d6ecbd758757216a10da7a96a010d62eb0bc9b88ad6e4d9e45e34b4080b37aa36fa7163e166dc040ba2449da296d7798da742b63624ce3c24081dd9dab57f3dfbafc6d00e2e9e52a3388e114914ea0082688089001fcc6d3d5e7b6f548d148fad118a76329545602e3ed01de1cd7aca8b7b53173aeb5de417b4b6092e380a39708c2f28a76c6c51f0b42dad190a11a62607b83ff0afca719b8176aec59dbe99204cb87794a27ac66bd2ce36b18b11c34dd388ccb7c282fc99b7fcaf0cf419b381e07c373fed17b72d7599a55f2c6b99a525a8a9d29a8317d1933a750cede244a93c540637e11873a96ec1374322b0b1aa7617cedcdea98c24ad9afada76959c451d307cab6935cc0b2d642073476c21518143be51a5c5cc57b78768aebf5d89388897cb835ef3da830d98b6c7ae104edcb54e44461033a5cd80e9589255373c812d1d48299a3c0f449006011a423952b4fb3da70721bf652fa3bb672e8ae122b8ebbe8446acf7de7b2d2614294a91bd83776a06bc5a21a35b3627ffe0f959f8f96d8fcdcc65ddaf583c5da4856895c86733d25f94182bbfc9d66d0447f873002ddef84890a5ca06144e1ee924e79234ea6bf3c1be4aa24f6c40e4c5dde525ae06cb5be7c2b5a87a009dc10a8e83d4994b4f5b59ae78ef0a2c197ff956fab19c6d21d032a219ff85b98da23b78263c5608dd0f41fa44c05981fd9b789dd962f4a946f239dea320c136d0dbe634b0e70f9bd88677da823cf1489760c14ca7ef1fff97c4f1af679f055f0379ea9d04d87f51aaba00e6a0d4118e256e11bd0a989f5c379f3a595cf9ecace9da9d9375771cc01bf455332341fdcdd189a50658977c46f7b71a44dee03cfd24d1bfd87a8c36c7769496435a31bd2f4209cf37e15adbb3f6b4106b712a40503f181e5035930c82edcd3fbaef3ebe9bc4ecf210479fd536c0635c28c0e9c4581a94737c3a92b41d1a0d4ea681257eb8cb3866edd27f396ac3023fda56961450e434bc8c1c37195cfe488f10276db55c04b0bb449d075d2aab8e0f8a187e75826dd9640b6eff8d9ef92608178a478108e19da575fa1b7b2230073208ae3533686a829a3ad50c3a711a613483b1351ea0686c2027c8f62fa95f2a6f0c996af178658a93ca2bfb6333163917c6c128c0e75fcd0373b0dd8c9a4e06f45a04c80b03ac36cabbed75ea50791434751413ea062ca85baf44aeef23e1ace3d05a935a780659f3305a7267c9cebfc73d9013ea7de07c1b86ed9cd46028d8855d0d4625e0d5279b0bf907fab6454648ce0a4c4007487c323057d7c92f49af2d01c62b6c93b884761365b8d8458cf4f86a3071dbcfab3954ec186e7da4f07b0f3d750c764edd07fed9b878948af4df38794dce47b1588a8983f7f14427c4a1108299b8573ba8f1ebc209f885ba3f75c245ce91546f122c787088f7b6f0527eede37dfea26d631f394ffe5df55407e4ce033b5c904d1742b0ba1b638d1f4ccfe1988606bd0429dfc9f84c3da8a81b785462c04b38c169387d4732ed90b745b8a8cfc7bcd5ff5e0e975dca77d72b3c8b5b8f33d408336a8a54448d930da0bee7fb2cea333aad0ef81d73ac48d295b444f9138679682f44b39add58a0096c161cc995eeec98cb54a4249b3438490932b204bad77276290670254aa78f7891ba5427a2ac151b254faefb7ae2e4ae8dcbf02c0834979bf7a14d904f7ceb5ecb7dbd3ceea5d5bbc15414d4046cf6116c191e35f9fbcc721289827f8b7838ba046e1779e7aa192c52bc8ce043a71691d9f4ad47d2d5d32aef66a75a8f3bf7495d3726eb6f06b769d399d32e75fa6ff25ec5abce063971ea5b3b2d0f8facce977b7dca5acb82c2de82f067873215baf96ac7b72ab76a69a1e07e678383b2737b0440b7afb126d8d821c8d2793b8e845735c07687ec176c14307cec4a1c8010faa7a118c703396ac819e265d78d35333cbea85b7d0aa1adbadb68b52ac16f1fcdfd1b96d40a67473129c1f39d1aaa27f6f07873604c7b0b9e9ed2effc9c7664f0098c3bf6d467ede3af7e0e22c53fb28b2017f9aa1d8d54fc2c1d04825b90d3abca5eb0291da534e36a088ad95de72983c4ce4fcd3ffc0efc60b8a6262113666b332d7ba68c5299c0ab319687e608b1965153cefff305308e9d77c7c6f452680504ae3318c3356319912b765fb868797ea3f87b5f119d294ffb979eb32b7109a4a6f8780e4ea109c49004d16d7e91c21bfb3656777e5e3ec9583ad8dd55ee4de665a748cf57bf4d8672f89a285cb0786d15608f0492a2b409404e8edffa01034e8d7ac6a47afbdf13788509531a3e135eca220df889138a4b07f7bfe5c800718fc8c673083ee7a823d1d3ae89c9767999b61c0e057877113fc867e865a81865f09a1686c554329ce16efe6ebc5a67deef6ce4c370a71b770f6ce8c53ae96f6cb339dccb2fc2fbe3721d5f418c8a43a1e7e7f679813ce0a8904cfc46bdab762c501fafcef5f49615d56355e31bc2638967e255c4999ce0cba583859c51c2f67ec7bbf53057c610104e334595bd92f0d794943ad15991c77ab5586658360d6c544eb4a3d6961d21b374369c555542905605bf8d2f9910409da01edf50065afb37469aba8415058bf43a83e9009c027083067bbc867359f9d9d88fb333b270777998693b15ca17187aeeb738245ab7f8a2cc97f15ffa936b7599d8b769e29a5ad533f3d68723db1be11ba91c27e9dffe403a37b9b761784c0fccf4e1df2b16db8290ee9a8ff5960ebdfbe262b471390424f05f8a44f6050577bdc31e111138bae495522c9fa38717af01af7a44267aea06ce1d0e757a0ffb8a787e70ab96672e758823ec57b9db237884aa2a324045a8734536dfc31daa28490695af196e3dd85b8c99de4a6412f8f2efb8507533116d06a28cb3754281558b6e10924f159ecdf8187e87d56f73b44624eff0c31bba89d7eddda08e000e359e99616ab377e3f240e511960782d76927588a421e4454f46780419c77b83ce6a2b43f7b4872a29f2c0edff9660ef79f991259c744f1a9664a9f50ab8616cc7d6120423ee606136adaabc3300bad86ffb597dd193f837db1951149842db98eb756e261d40f7ca861e4f629c98f6e79277c9c7396d0d5f067f1abcbf5da6014e789e03f1fa163d012f961d56c6e5c0eacda8dda3ad3dccd40089ef71f71ecfcfd957c329cecf3645c24d699fc8a539b751ace64c1eb7505c9fc0240a40bfa0a850757059831c6877fdaff654de04288ac53b81d0c878a7ad9eae7f213d5152d047f9de2dba2bbffe0987004f96c826f198807778e40b392e7ec8f0fd0cb476253995cbe9a46cd6a59e2bdf5528f4365d50a574aada2fe654357d14e262036ff39cc7257e80af0ee4733dcc77cdce9a63a820d704d5e00196125f865426678a958fcfe98544d4ff30ca76466f22e99de29c4e565befb61ef44826efa77cf39927c9fc6afa0d957642886ba8b97725c4407e2db1c592430a6ce8a07156824b0415680579c12a6bc84bf44e394014b4970e3e66b6a8a4b9826a0a355129d00446833e351e4fcfc1c72497caf01ea0ea99302f0c6b49b3bcec792db57b4c173cef71eb610d62ff483b5e8115ba4e943566ff2446d6725353a1e633347bed5d1e736ec61f030727f83bf6a5ccb92482ec5f5df171be26eb0bba5cd483c1dce62eec034329a4380656c38b015e56ec93a3044d18fd1ec38d588cb0ceb8ed9e994e1031b6a690a712b361c2bd0fe7abe89536eaab1cac1501bc197568591cec0c61acdc144846a88191740518162af410e5391650629bf3cf8228b7d6e998b3438d29c033d23145758da395904b361d5da2fa9b3a9c3be0b41c69c2180ab00e428b66051c6ad2c1fa916d780ac90ba77189fe4a73ccc863cefba3ae2cbd3b6c34be9c89b004dbd9151fdc68cf1b613111e8abc88016acef8177604d30e938c47847e08845a5da148d20c4e8747a3082f14208a48a765f31fdbe07250d807be13fd8c7e528b489efc13cd951f8f47ac6f5816e0599538478700d7e576f203a763dbf8dfbd5297c22a55c8ce35133393929e4db46fcfa26a0b52b3526011216f0ffeedaac61b8afde0c44a331b3f663c83907448880bd485295af89fb56af95fd6519c8300992428ef7aa279faceb93832f5310dc4f2a081ef8dc1ef580d21484104f7c8f693879d9fda2a7da5376b398d0ab66b66a7748896280ea14ca159f4cbef0b52d1a243ffc3d900cd1b35ab3249868aa0c4704fd70539d029fcb6d747abf7d8198b441cdcf10fd1272bb5e9a8d1cc81ae36d3763f6db19554fca6941e44499527555fb3f5033d690d4a419143abae27199c424a479d43a23a4c5e4e022990579ca8f9b465a0d14674bfd9225cfe04743801c3b5aa8382adc49f6f74b9dbf120ab3a348b910766d33f811fd389a26d8f61a16ec174255a423e98427b8437390ed8dbe91ae08608752cd8c22f37e006a87410046727f3fe0730a91a915d87a107c0f3afb8324c76e1e923b362c637e17965d4da3dde01aef670949768cbfeadd5df1eb4b12c616ea96b48bc6a887a0ef1519512f32445f34e7ecd696de98023074254425c5ca853964cf48ddcc0c2219e047c3bf8c7465bfd90fe4b08edc12b1650b9581b208dee90a9410a2a9e262586bc067860f130ce9d012d13df06fbfaa94a3ecde9f046acfeb1a8c7e4b9800b9e5e752ead72cf36c14f0b4c481a3cd709822777bb14b62861d93eead339f48a4a426db8e5f8e36673277a4f0f4bb6bee5e1cd82986407df77a397d1b1e8210a6a2363e3a8035e0ef42e6cf55ba2ea823eae58ff86b279a68e751e9337a4ac555c520335f58989eeb95d871fe77e9e9ed2233bf64fa80d85d8caa7f3b1ddb2ce4850207d32fb58531a070ef3e2b6df13b70b9da22a8f5b4aa21230259f28ce2ed624be83b48f4932da6dcf2cbfec17a27777f4d5519081a7895aee805e5d3a0af447f7248ad7fb30208feba90da975d5c271a48a69fdc00cfcf2c5db052e58adb7c8c442b8cc4987b803aa42f6a1b2e5859aa76f75a526571e7d884138fe4b1abd46cfda5dc0dd14d6f921d52bc79dbe18840b9ead54ae06605d3b7e5b281eaca095f84125d56b77d50e363e9947da4324a6f1fce74bf95dbbfc5bc0d1317b0a3c36755f708cf5ccabce633c5b4e8fe178e2bf03f983b4a5ee55b26e545eb845def98402322d58975067d223aad238bf21d0c16e2664046331659b44abcbd503286672d183064f83091af43b0e266e05d7a4c663c5921b75a9885706e9d519941e545989aef7bf8329a942f3070386fe707b52efb1430896ac7c824c4550da3b5fab041cc8b1140746392657842259e9c9bedab0ba9e60597d034180b64e30b1d4b02de382835f83dc34c5fc98751a68d554bdd26c79f6728e736045dc7645f16bb2631dd2fa3376afa18edbfe6dab36679448abe0f5df363fe2637bdfaddfb9e4d297b0fb4df5c71944312d3a89615d2137e298b23bbf87a3592ef3754f8a0ef1f2052057827c6172fcdfbb26639f1b43579904092ac1fe5bf4002dc3471c1b17e9b726c85ad89bb0a28fc732036daa18779b13cf0ab016e00d14e34958bdd03b6eeb8e4813e4391b2a95cbb1be85730902e4cd59ce308d21c872f99481dd596c9bfb9a51e50239f3f412fe0df3e0790b09477d96a67ed613174cd8a6133d4976d026646c2873be56ceac6a87431be418290dd808d48c9a8a3baa5a7ce55758c97939845ece4e022a0ff7003f741f7c38ae02acfa64a68456a53b32b485100db42124117155910c610140a7d75094266312fcbcff55097782d3c487e8e366b7fa3b8e984ea4010ab2dcf4a260e13763dfda9f90dc89caefa553c225275b9a046358eac074adfe7184fe8668f3812ee77bb185c3b88071874e0073750a9cd8428b77d42250c6b7e746c111d595a1959b5281cee53e8c6aab159770ba4072c5aef1329902b752e6f413854e46ffd7295d77d66c7723a70652e771e6619cfd2c85376ecdaa08135849656064d4d9fe556738c56f2eaa8cbd4a65ee62e60c65fc8a5dbc555e633fcc046dbacc8a3afcd0f4031bf22dd3808367c6aa93b344ba570a607478bfb73fa8d4b742611072abc1778951aea8cf2473dcbc0a30a5bf18e7d6ceac660b6e9606cda7029ad30f954eac4a93705842f56da87684da2ef5f19ee7859877bc267190e244c598cc22c25885ca97e9e301017b7c9ff892054f9208b32b9573f519f02ce64492c7a1a27dd79788d25716c90b5df80783cdfb7ff6cd6b4b943456df2f846000a441c158206502e136814aac5798f45726f42a42c1a3b3bf7f9d836d39311618e67af6df51f0e2ffd32b0046eabb09b9154be4a41b36f4b9bc3e7d92ee14fabe690c1eec7b3c84e67e6fd551825731bfaae5e67b2ae98678946933e3ce606d71ec1ca4bbff9ed5b5e12cae242f32dcc4218a7c7a6bccc567e05eda3a0aa1780356bff860cce5b6c52a9b9aa6024378e374f7a470c5eeadaeeb359a1696f089f84ab465ac011cdb05d062bafb4f6faf3aa10f973044b274cd648d0e86adeffc792f772181a72c6df6fd60538c330456cf874e36a7093f9d22b2e17745b5a79ab2c3267472645ee73c3fea9900d69b3e72e43ecbec716d3770bb4dc01bfeaadb74cddb61a6d5f8d0640aca1a90859bb4098f51cb039358ddf1970f19707a10a44a153fac4f10d07d2298018118de134afe77633e9274994c0ed8d26e10555f7ecfcb7b4936d18bf1e2dc80a58acbdbb53792ebfd19f3c3ccb0dd83f601daa0beddeb4c3551838b61726c9b27f78aab5b6d8066a20d1c5f0672618fdeb820362b25b4c37bf0ea0e013316a8c64002589ec4f404e1be368b30bd6a8be9a18d3129ada969b0b31e19e00c3681939dc46035a54dcb1aaeb8ba895008b1f697396d1d4fd27823ecbe09c7fe00ba085045bb6c4e390820d6df4132749377285cde6630707bd516a50122a130ff9e825b165280512277f8dbefa43eba6b594022b97d3222c33035cb0196d9080548551c8db3f665620cdde18d4e8316b18cf246279206a980a1e20d9af56cb755c716d706596047798c8843ff3af2dcecf89827873a042a883e9d8b082f4224c624b827ae356b1e8b2416e6f6fd0b403b70254ece8e0765664aaec93499f467ef310094ef5c866e167976b87dc058370ebc985c7d6ea7c74db4bccdccc388e0f6231f59880cae7171a10e564d6c534c6fc956a301e5c48e69366300239729ce8cf835663309a32536bb28c7c6a7c0c8fe1d9bf63d4f097c4db093038d8d8f8c47295b03ba9f51d78beab4542a7a1fd8b3523b7ae6015d904ffe87b2d666480d58e117fd003914b01ee29c6f0d2b7599ae4a1bd62baf5ae651f74f21ac3ce4dea34d59e1d25ee3b75c4c6ea91cbac3547a2e3a2a3f1953f00c2029d79463ba6f30fb61f85e7cfb2a893ba90bd469532b2de637337169564f0e2fd1009b59e829c6371a19729447c087c92260423aa280a0062054e569f3e1ae96c99c6854067d32882e7685a91af5ba9cdabce77a0d3c2fccd32891752b47cd591f666d512bfc8fb2fd8a8410b421395dabb2b9487fdfad81408532aaaa1b7887180747100c04c7f78b2101b31e1b2b0f0d8f104ae3485fbf5925e7e6278b1ef08ab3088d6375061f3508b8297a8a9a2d1a97fdf62c963932083b4e5b6018146451249877bd01b8df8fd829ccce94ce9977111a0ffbe1a68e910548f06bec306fa05742eac365289483969d28861aa3ce78050d6d7d9bba625b3289b4c87cc18d4d9445d9c7d7344d8f561c007054a672da8f39ff3e270404923b34ce6aa7723310faff6bd0246ed5f6f860065a2511b84aff60058e3d67b3177f8573ff1faf2b14d924c4365f3ec6bad00b8979145e84c52a4a6a01dc1e1429e3218e7aa60ad6f35e9d030a51e5ea1c0309013dc32fe29f1447644bc09a87c40cef00323ca925ebd45d937a567c505286bde8f4c549dd3dcbc758473740c60cdfd1afe276971059b16fb2cd2e8910444d45b3c291c490ee02b202fac213d1f4ebeab5f9751fa09f27978f5bd803a2278e4a97129f7b4d2629d9513f2b13a5a11928f9c46ff07883cb1789766be6576f32d2086c1cf60d87e39929fa2293ea3f2b2447eaaffa0621163abb2773f8fa886fa5f834aeb7f34b61f4852f3e713b8285f54232cbdf39be17e63727747ac8dc6c168be48b1b34b5b80acdfa1ac19058ead9d01c635d2788598856ea91ac8b3bf45c4ca29c7407e40da2d14f48ee4902eb466060162553800ebb678bcb2c6784b03b3726d9da2b83b58db40d36429081d626e352bc763c5c51e8d0a5175e75c012d32a7e71b72b161e38130cd8edcdabf56c37824e3dfba7a90a8e7832a6e9be12266a3f5c91b0f6ca738562a6ec0e9f6f8c3ad25ead861469f4a39c68b0e461cb7e60c1bf80b2c8fc95d202d1359d9e3b236c199790fd261126db606311c2727bde5c18baa2d4205886f1c51bc6c28ba62bf986c674ff723863914d0cf4afb43d68139c60f8994ae00a22a9d9cf4685a5c922614669ee377b53333d2d86d64088090e993fe8e5ae84762d1457fd85eca536b72137df9ed6f7d7d1614ecf26d4054c919300ab83dae4be3ad399b42e6c66a7b69bf865c79bf1594d4a5cb5d6647da5ab8be8fe6c9a762d69a1002fe570d33e6fe0bd28dc674d49ad8d579bf5df9c8927b72298eff97fe7b0158d9568dd223d7a11deaab2104dae5b90a8b907df690a89dd91936bd3c154342d546bb16e5cdd4b1ff19146d3977e7b963d5c00f90a9a34879e3c4297b1c791c2aa340ba8914c0c2b5b9aaa395edad0526331d42b54cf5c9bd71a08127f130171f7c8ec2f96bfb58604820414ef47cbe01e92cb47307da0979da81122ea1542147515c8b105be699b9e7cdccba04152198ed6d912537e471cf7d7715caef5485998a0d7fc45885dc298c655a182365d514b1c321566cd17208180e1044b49d79919496bd2b93c24d564b53aab9704842a3d1d7729a24cd8433dff3ce2ef06e2dc8e6f02a72c988dbc153612a6d2c07b3594448bb33844ce40fd4364b6f68505fbf63a7de1b03d06a94bed84e94016a22d7bac9835d66687c3ae0c5d63afa4a3ffb7e47e0235fe41bf36482e06940100f8a34edc261bfa6b5449659cea339ad503df7d3db1461b84cbba16797e968f855dc7a7dd142b5735c531f3003ca16b71a555b17e411f23b69fa9de96f79539be5daea0f547d60799bfe75ad5afa9845ba8ffbcd9ae2f5e2f969826a79b0798beb626100e6767cab513efeae6148e65f57f63545022af179a01ce3ae6a788b69478f07fd71980ba2a3aac5fab9dc872544c904d03f2ed403774649aa4b25dfd1c649a87df3acaba601d006bc6d506c0e49014552476af31f0cf819214fb607bcdf481ed398f50350d6a02d0a7819a077c298f253674bed4baf1959e0e0b816e4b33b187a425bbb1c6394ee84b3cc6c96707a36d18d30d396fe8540e8291e99c7c1a62fd3f8374009773842c9711cd51320b44894567da041fad8e431c2af481ea6fc4865215c260bc4cdbbfa86d17cf31b7bfd70541470e86a42bd68427a22c17a35e63080c66a559bca1e34e7367d37c4071f4542ad413a1b2d7dcdb87f394b343fed9f23f0937e67468812be032df402575b3d00975c5ab0360eb034ef818b90ca18a756f72ab6a1dd3f8fef3e7ce3f5d0e4f0acd502518d3b3b51fa2eec5b6a5a4e28e811c799ffe18a9be8be9838b9ca04525e9d9a6f59363cfa56c1dccec367a1dc7d3196331861eacff879f8cb999c1c744f12df47d77079b7f8fce24aaa6f3ea77def9902251dbd44553164e99b6f0d32e9bf4866ee2da8416d73c56ea719fc58f4266c7cf927295b7461a9d53ab70f9cbd02e725b22a9ebb63fac2af3b37795794e0a58c9833b7a8c0fd530811f47fb748eeff12cf2417abf72e06472526d290c5e4a137b182abdf8eecc04ba905c50e9acca145e4ccc4a34c8f13d51c9ccf49cb468258e27daa81872563dfeb8bef6fd96f2481193bcf1658ae836d17bfbe8bb55c4c65dfe1662c2b33e804a6c4f71245ea3a193ca9a679e1d5bfef723049799c0414cd965590cba1b71a233eae9a59b109f071ae60b7990fad8d36ea3a972c11ed223a464e966f494d1f56a876b01599b995bafcc13c544fcf07bfef393ed1b8d43f1a274de57dc3e7735724c9b73309cc1c0757facf842a4101badf401867b2a95263f66087b58a7ad470077efcef6be84ecdfe60411df2f0600342d2013559844eff036153c6203daf94d2013af03771eeb695ed512c7adb80eb48221e2a6001e8e5a696daac917f515a1c7a0804e74c2eca2d46022b0466e926f02f3d20d568277d68e3658c3a3d1281ef103570cb6eaafa561eceba9e99cf10bde1e176053ce14f42cae186a5f12762684fbefd67f36aae983c7d44f0c7eb632f0f32f0868ae21c31eaa6723d21b89754456330218f69e8d0ec451b901dcd09d3263b5735da5e9ddea6dce77f7da912fe65266942f7fbc0c377b14258e2b1325814b6dc5f9f0929f27a0bd0b06206dcccd0b521d3c914e32193f464dfcd78da0a008fc8ba46d4aff2800e5790d186e87252860a7f3880a8d5696f6b3a98b777fecd5dc7e9ef42202861f561d952b51d7dc1197a67ce8e17f8308a8ccb48fc73075b6f3153e9e60b387f7ee5bf750bd8d5bbfc0095cd12e8509261a47e8e3fea366876b053b418a490e291a7af35ef6087091c6a2309e4368707a0d7d241250f22061c5a919b30f851fa49ec5cec982dffda610939d50fe89bf271946836f21c4eac3a29a77e4244ad30dfa394e1bb055938fe4c61fe7b7b532a144f48c3e446d7d03541d86a6e24d8939474eb75d7dd6b2aa9c9fc3f9d7ddd2644d410576739feafbd53905b4302c78c355942b3ade14db22cb2b513eda138dbf8ae40fbf9ca03912d3da5404279878ac75eebb9163d9867902f24250b5d04979b1df7d0671dbbf1faf5bbc55239676acfe0b106405ca3dfec3f20dca6e1afff3ecce80773737d9473631bd837cb7beebccf940b1727050ed46c42616023ea736b73bc492fff7050bd3c95dc18cd6b830700297a3ca0128b8f35ba63ac8ef71e35c7be5cd77ea52ef4058b5bedc8fa4a07a7ecd44d520e59181294708115a92185aa1d7b5429fce341de0f878b649954d8ae7a74dbbef1506ce527a21205549763b3415cb14e859c3cd336964788e4a14738d28a14a6fa735ee0165c776e1f6d11bfba6e8e29a32c027cb11720dfdd21ef6b53540f1deeac47a5b667b14fc19e5dd0362dd7c74082fc7191c1f2b5b3dc37dccc27fdad1033bf25b7d74e928a72a07d568667e82a3dc7b47ab02b713372a6881e4fb55fe2c8d81ae0150e9ce2ed626900649e878ac6781d5c65534b583421485b32b5f95950908dce67cec98a1aa5767aa7d6cf5b7094403dc62ac12782940760f70edf7b0cbe358a2e91acef09f3aed25cf5fb91f73a3e1910039dd374c23b96c5049582d465821b598fcc74cfbd7f85ca538c40cbb1fd45c4e7a3ea1f1fc0aa38e2d5bc6f41f1d6647fbad0b3b5369ef6bb2faa9aae82cea4f93053da3465c273fc2cbeb9b76ed50e3b2a57d268350ba3479b535d72fd083452803a9dc93209131da79955f6ff89008053daa5b35cb6d68a47f055842db4f0c31560525698a7cf116ba10e52b87e24aade3c9f4956c76e07c00f00bc38b33c6f3f1b961b72382fb3afc69a6f34619c78bc6161b0d8637229a6b273f070e7feb1c9c2a7ea877e1095cf04acebdd72f7ad393e3ce252a838ad26d60190d84449a4c3b24e8836fb7637a4b2a395adc3cbcf25e02cfe0b6c3d5fb591c5734ac237d95bedb066f1192323722172301dfe3f5c839cc8d23d0aaa4595bc630915a1f1179102e1e75260b4e4490504af9bec9de0b5eaeacde484720852ca05f6dab3a9b63335b6ba71f2f232aaf976e653dd613064964fa85169aa8fe3647470a73b7b24cf4cea36085332c2c47999d3908830b9144e6f1190503b60faffcdc3a9a4d84fedaf367656c97ae74dec62061051bfa388b946add05e7183af690d3db5592df518236d7bf901852ad41d383eeb22186f8927ba888515c769e29106d68671f706631afc23b1ff302273f4ab327a82e7617ffa3871313dfa0cfb38b66ef040ccc6ea651470183ddc09ee444b20c4b6bda9650d41906c9b9ba4d685cc781c717d11d2c4c3e89402ed07e0c83df88902250f90b7c0ecfdbce2500c492281c9fafca0b27d21e8175687d7f068449973558b2ab1dbf22a1b154c3b4833905e11eb832c9aeaf0fa273334bea9d2f04b22c7ac66481045e13a50bee17f00bb88f32e2e727eb1aa5f8e2e4b294cd3ce413f57264a30714e66c0fccfd0dfdd15623355a6dc3f4109d5fe8c24104f11107764dbb9eef60e4b9418b1208f1fbadda12eea73bfd771527f84c7387a1272f29090cb2caa2b813d24d61b7f179a22daa34ffdd5472db181ac1619242733be8350cbbfe89b7cfe18b6f158d5881124e7ffaef336259c650027a421ebe8db7742717e895b8dca03e87ff69d07c2587c9ecd8f07bb0df8ce7a941ca812ed0eefedde34602070f2400a0fba726a3b590d7081fa861a97e71ddbda5bfc0b5ebe80b03e3fbd8bd78caa251176d9d34c4f8368c9669faa1a8460341ca63444dd3d0a06478dd563ad0ec984f1d29418806a1f6048d1180b8038e68652f5d5b0a8220e9a531d2670a7caadf89547037b55a675fa53b7c8da9793a3ce5d265f642608ac68018716ffb4b29ec1952800794c7f28c9ddafbac43161e1594290f6194d34527177a978c9bbebab9f23bb35cff8bffa48b5b0a3d967b99f85ed0fa15d3fa73a2707fca4e44ea23c90d6697bd27b5ff982cd728f2713ba9dfa744cf1761334b3e4998d2c27e0b2287e96c5a81e416f3ed213a3d4e6334eb09598d48ebb4bf2677f6c799a5ed03b42cd3315fa0db226a079ae144b3a243cf03cec1c56c17e089077293cd","outgoing":"ffff06bccef1fe99e069b3e4119da5e9fad9755c4199ce2dd7ce44c5a90eb6f05f962c34423836cf55f462d452316d685950781db39d5b1123824cba5c1c1ae5082eaf72fc82e41b65a97c986d5702b91879a72fdfbb82593ea366d0bf880cef64ecb8b8fa7f8af04cc40bdb8cba6f4b06e7a5a6da43982d5c651508ca8c25f7a42e05963440614fa948c8dd66fc227b033bc6a5e4fa4a6d145dd35ce5785a3c70f243c016c7a7c54c9eca5e160a05f92ed7bbcd62fd83e94e6e8ef4d3a8e532d1324505dc62430d1708dfdc4c88e0b670cab2f4f90fa8738ef39e2c356bd065cd5b698a8af7cedac20f6812bacd7d948af8cfa7f03ef72f12ceaef4410191ec7629f0bf67af1cd7a2dbcff662c73164ee1eed34e1c12e9a7f1aef5256fc8c485b2c539fe154a521b83033aaab488f377898f6a010c2578698280f76078d71fd393c85a621849d1b0a9d460f27f8cd69ee063849f6b117830911da9cce98d8830ee832f40183a3f2c1b07417f818c1af4ca5d43309943c0ed083712c3e975e7c25b1ac8ae616eb1a09b9bcf7835f562bb535220b964febb5acbbb385bb793a6a0bc3bfad274d9201d26622f0054177df8be6c8e91b3d051d61f0aec13f013786dda5b68ce08f98a9f203319fc5067f1bc3f71dd6dd16163696ca2a1c81e117a20847c0c962d9566243c02a883a333a727c676cc7d87a415206556bc338eb18191e4cbee2e57633777d32164d9c2d1ddd94c3a1f7e540ed7986f2b6f73914db7b6be891258ec11ee426828236160d7c3fa4fa657e5bb78e73be72e9288805e0b76bfe3414f81bef7d158af054271f5e78d04e229fe54382b26d7329b4c2a18370bb6722576295b51e6fa345c091e1d142d5bde2308a2f29b98349256a84bb383389c2f655e930b783178b663162168b9194daabbf547f456bf825aac6b8a6d4a0836f2ec46f3902164676f076b7880b3657f0b62a3b2e0b1bb8649919843738070febb52d7ea8d33f81aea22fd7f082777e0d27a7cd727f30ca4a4742f016669821de33d7909429afc5a2908c8ef63dde5a2cb31f5bb1258862bfe7d330efe7265ddab73d0c3c5ac2a949d3c1257572e40c29618246e0eb3d140cd6e333802aa8c45c2494e2ab894dc30822085e470b2f5195c16101d15488a000bb8ea181cfaf5188e57a95a59e90efbc55cf82dba96a217cb237bbe5f7407fadba729617f953eb55cb45761e77a74d9622c85d080e33f30265e25cb8990400aa43a00cb9fb124645d58e2dd89632534252426b84abed54d982584a08da6e40635508295ea49f2804a13b5b0f1f042a4f816540548af1a9abce5ef40506e562dda0a677f24f2b03c6896f5fb2b8267e3144d8fe6210cccc8b8167d726e88f511d5a6677a38a29f1e41813b2d29a2c39a0b8d2a5c32aab2b9c1f091627f428edfaa0ce60ab2dc6346207fe73012e07e99e6ce2054270f4f447bb19187e3842de5a703de954f97562be7fc64d34fe948bf419b4df05be7d725aec4f161732043fbaab3a9837253a826a39a1bde2097e10a151fcebc15abe98fa9f7397b2daf4c41fa912bb06ad8ec0217c979d464ecb7007d9d6042e62c360c9cec1b82f4d2b8bb54da3e057094e979794370a999e43261b1619b4f533bd76dcbd4b971d900ce329d7c922fa4062e4edc15838d8798574dd4a439f229b1b022edd694b8de4f3c517bafa933347fecd703a8528a29676c4d1d3dc8e26b0f6747f744de59e37a41dec33042ddfcf2ba6fc53e09273b32c1172e7c88e7c67ddfbb5610b6ca9a7d3716bf321ee3bf69fe142f70fa6114636329ac4d83c534d89cf97be2af52a290c55f0b9e41a486b3331733c6b329024aa78ac279273620d870d74c484bbf8a49a71a0eae884abdd8bf85595c0a9caab3cc3b2e387535728d3ba66278769da00975c88761383455a6f76fe298a5c5a91cf4d157d199621dd12d50a10a6ad696da446697b5eb96adeafa07a746a3bfed91a0e9143bfd5e521102f9a440e9d02cf8e010d3b32df1491445dc854f88ab5f3aac01c61713c6c31d3a99e2e49094bfdc1da2c754b3bc6aa7444f6f3717585bba32e110810c448da8ed495d6414e47037c5dfa7e648438912d05a7dd3ed5c8620d36e93d11a2ead963ae6ec0aae88b85791ba0ae64be127c023cd39b5e68e79db122d1976c14734c4f9d84b4f0c98309ba0aa5a9d79cf55b357eff44317987ecab559750a2b4db7f11bd64acdd4e39e66214c57f4d360abe69397c86f1047095718509944ffba4397ae87b7ada381ce2e007c1014134ad1cabd0a6acc9023aa60d0488251444d5af9af78ebb9f2c1b92e9ed48c6959ffaea32eaf8d65068b120558aaaa1462c96a60e79a45919d209d078782581f1c6f63c427af2822b5af38a64a506f87dd24775562070c25c77aa331844e0e85f83c6a4584514879748a3acf3d38389c38e7392ceb83dfdeb77dc6d48d302189bf61ba4b7bd4cf18f182bf584635d5b8a253ea56956135f152ffa9c1f6f5583cc17a0d7786ffb5dfea66a2ad6dc7aa38a08521aa331b8557d97666a1b2050974fe477303a47cc74718717834e720b64c63c7421004d101cd67438d41593db1d7b46a18c06399a39f4cf7cbcea0776636003912f4529d0cd05ec7a3f4ee7d4cd6ce940e3c2badd4a725a5e37540adebc8cb8120b6387a2678e02314b06e1e043f39619f000dda05d6baafa8cd29e606e22dae0257a1fffc0c7d05b6674092d73461ce6384c6685e7f041a887392612f68784ec1a0a69013de8c8090b60fe77dbd12e79e4a28a5e3fc5f5b79af78cb1eed9d6c89cc9312c1efaff44ef4f3f315023a98c80971d4411ed946762f114bef6ee3291600cf660aab2c161be6ae3d39f3ae27d35d75cec04b1ee02829458d1817cc4c79384a1efa10a6f4e15ed31111f24d4e7e7acb25237b52e3e700fe367016bef3c1e556d320776e6834c6d72f91d8b03fb3d05574f05caa2b422421adda4e6a34546b6a72835e49b3781843a864352fbe3b937dbcb37b23611a45cd54f1182876d7e5731ab44c08596bfb46a04df92978a9404384de20eb4df22b216c0a5b91a9415142de2e08112ffbdd9da938dd7b38fa64d2753bddad7a03ba84c61c898be78a92b7228afdb798f0420849ad76bb3a677cfa40d4f049351de0bba34c9e42a408fc789ea42023032866bed3ae4cbe3760fd66220c64c9225d2b6e4af72b9de5985532164002b86ebd72c0a2404974a56186cec2749d66a28bcc9de14d1c11398fe721be94b9bc4f2db3cf35c720daa90fc6720c991879115587bf5a6fffa6f5bfdcc8909348b018081c80f51c81f6b662b3463aaef929eab5dfcec455a7a65de1f28de2b06710658a28a926c0ad141de56e90c0a10682d990131bd4fc83046a5728c36cf56c6fd127caf6bd257e264b044954e500dc9f452fdd6c556f1a45a210ee853c3264b6e21767c312558205f68689cd1bd4a19498b887fc2353b13b869fe586c2a6630deced60a794daba5460514e210460cafd6f60554e0bf1f64431f8b8fede90a4d0097b791a487ae734a8c4e0e058514ad5ac28229018760bda3817898a5cd88a05848aa9bc9dea07596c86c0bbef884b25b715ab2d8f528a8abb2e691225f6d6d4cb3eed245f5b246c048553648da9dc442aaf26ab628cbdef30755ed8989d24e8b57a1765a374b8d95866c8212617a551fcbdc211ff3fc156dd848eff64b6dac7ccfeeaf59d7d7a05acbb3efa0bce5b8f0b6d7bd65f8a894398a8b2a4a12761a6c1a414a78be8136756281c215c7b3166096280114bdd42ca66cc669d518f4237c9706ddb622d02bfcf74c754fac06bac8230953e1c1795739b1dc0cd3369fb6e4622fe61e6fa2295c6b56509b8d30c66c10711ff5caee27571c0a188a3fefb08cd17ea3035a0d18a5da715519d7ae2555dce9cec22be62f13f26f2dd8a9d051c55075a5f895bae8548573de7ad4809e46c14ea174e49b0d495f5e44858909d7fd1f7ba02e92473f0708c258f5309bd80454077ec4577726a22bd0ac53ce2e71c3af77e610ff2bfde562e0d6dc099079de6ff97f264cdef3c759b2e785eae67fa3a22f845ef8ca0f503b1609b66f28565987abe3edfa08ecb24c4a933fd9f677a8cd8a30919c53ce6df6f2ca78a31df0d0b5cf81ce4b1170b0895c1c6443e58750f065df0ad594fbae90fa39ffa39885e83158a52c007cbefdeb768e4fa230cce0c311be9b76181fd6ebea31d9b5248ec7c0823fcd4faa30a17198f35fcb677303c0afe1c7b4dd7f0c992d7b8ad180cb60099a2edcc2632fc2dc9a2786eb47bce2aa70f56dfb460a4360dcea49cc8dd3d47d8ef02e460d2f31f5f66b5836da9cb14058e1431655c77a4014f4a77bac05bd9b6777e16ae03b2ceb8e34039b6d8d48a498827496f6e7a285fb80fc2290f4c6808729d4f1ee561c119bae770242b73046fb13ad30fa60002ba9abc6ca73f0f63ed60cd06e3aeac8197430f8edcd62096f02e33b3317e994e617d47e42617d8e4d48d756ebc8d090dd4aabb0b948d02e13a681e4b83148cc93aea231f2458dc4144996ab180625a7b9f740a59e8eb2464d102329327f66a1d085513f3849f36dfe525847d411a132444d2e7b8425b363aedfc26c5409ec46c36b929caf0c54e1bf3bf5a72095301063c68d9824abed46420718365e91d3cadc1b49bbbea52a12461775d414e6b87113b59ee48228b087688e9581bbf51a81f4c33f1613d491e4b933e2dfb434a8876cc2846aaa4620620d941ba739fbc19fc2a8704db149307fcac1cc1eff5924cd52f44545f1c83ee5732831a53e341697118c8b49371d75d3c86ee40565d6feb8ef039c758e8549e64c83bd2add4a18311c159ec122f94b266a25a2171f4275668402e317c6746101d7b1376eaf0e3e85d96097dd879e9eadadc71ed46e8b321a3d5346c22a873651d8627717995e947a6b3677c1bfbc0a449b2f7a0ed87b10e7051346578b27af67763198bb7ccbccc60f51bbc0a9709d7b948ba3c276b2490b8f2545aadb6f891ec4c0e289ec56ea85c7ab6c46653e21bb6e4bb20057569454f7da04cafcfd17fcb32c8832b606433ea9b446bea17bfbaf029aada20fbbd88f94f1fe7a063531513249cea5f395a20bc23180ce122a5ea890e5c3f6df0b288d32216cfa0d505db9e9303b21ac3d0b8a6d8204b4ff5fc3a91faa232eb978982cbb4b88926b1b0fd0e73a845a63346bf3fc4824f6c88a34f4260873a92a190bae5e00bb5ae37e0f6cb51e6db6d6cf2f08a7eb53d197a931f2542b5cee4393a3588fee088d47b95542d822d5cf9ffad0c14678a572ba6eb8b22b8d62408b666ec392750680a76f2383b5ca996fc5e936b837e2e79d23e77a198bd1ab11b5ba50f6900bdb8ac53e28297cf642b73f577ab04ede9b75d81b7ffa8d1de28ab7bbac4670341fd73e9a641b18a0c75f0da7b15d6620bbfa6ddbefb12ab6f0f131f2765fb579a8da253dce2d6b009a113204bbb30b3a35f35abe27d0da90a843ed0c49d9026ebfde78abac6252f012ac5b3c9b6d0dd7b524da50d065efcd232bdcea449e09340ea0f02d5bb5ae83a05490ca700ca574dc8f7d9e5edf3110ce292e2377098f6cef91ded8266e20f780e810fb79fb3004bc7b22eec7091670e0056f070db14130d75e35341142240b80b52da52d80f29f1c046368f61af25bbebae9205c6624333c9bb314e6185ded8a10dad0059e524b81e7eaf53b91733fd4b2cd0fb697ab1880a5238eb93e2cc902e07a17dc98c2a5cfd99d1724fb2a14abc9f038857f18a443d01947629a0154de76c2ad36ab2857bcd9ed55927f92bab01f7671c943ecbc49831a765c1eaaf43de5b90f44d199021be3eefd7eda61a6d1b4541e946bcfe981127e4f8e0e6e88c4a02f65dc4ce5a627235cf9e664cdc297b89e04f3e1d46263dc41feb8613cd325041002256d8476dfb5ce68cb1c36ea1471c41e10063df45ce61fa00cbdcb5ed773d158415db764346e16ffa23a8d9ab814a5e9eeffab2aaf0ed9f3606c132792d62613427fee47062f0321fe43fb3e974146c69509128db97b05d7989638746bbdf8d54d7a106b0ef203f0df5fa2810ebb7f57905241f4e33f65449af525688a87bbfd28166d3c996fd5ad05c88c6a97b83b038c190a771069ea02a1bdf3a9e61782fec89e33126b873ed339f3c0b3aa9b7a962fd80e066be658bf29b0bec9068a434816ae4ca0b414e45523f95717f1edce23d66a8e042537a564139b8ad81cc74817446f415d48a9fc303e5646f0e1ffffdf50fead7a63f9c355b6cca5eea8b6db79314ef5d367c766d326a47f78e400a5aaef23042a9eee2f10f17e244e656f3ea19fb559ed883531b0108d8dea27d648d5b6e8c210c988491a3e7d37e0fba4938b91a72d047a790137baa1e0f6091ffc9b9e8313aa8b02d27a6f91b14599ec53cf0863d71141fa96847a2343a14abc621bc7db73ff313bdf1857ed126c7b3188c3d14cf2e94f96f749521aab3a00457fab6d2e0ee83ff586a7bd849eac46e39a9e4644d7065ada35aa1a462baa0ecfefc814a23a4b1d9b1c5ba6ba1081e00e8fdacbccdb81b4d64625f4067a49e6d99979138ead6e6537c4af4e7ec149cdce6b8e44c56218bcc228a54a6637815ae8780329461d819d040fab7eae719f698b66b366520ca6fba3b4e14fd2faf31249dd5a16219e73ccc1127e435315dc437374ca32241730cadd4c3ee67f87df619cb0c11f0b203f235cdcad6518aeeb626134253a5fee557316756c398714db79846edaa278ebcc1075cdd1c9f3e60338e5c1f5edbb9190bbde19e50cc8ccca5ad081ab0162b84e3fc74ee2abd9814afcf0fefcda5d29595f1ac3387dbcf55a86915243921b78d3d5fe84ba62f44a559126b0b49b0b05c9c8808546593fd1d4e485feb78d02dde563a7199a7445d728870386273e7aede956c23f3928c96ff63ad1c8e1c09ade4e6cf9ed81d9d32a6f6b5ea00b552c39d27d46e9c855053a9616ac2e2b6d972eea0f10a6a71f376be45d7ea36877bebad9c63ca6418d32328493558582f26f02fa13db60fd943cddd8ee376fd793a79e3613b13f47acad425132ebfd3743182f653cc403390ba2f8aae01fca31dd8c9638919d94730c136f586e689bd937c15a17eb6602b10b36fcb3f8bb238867c29459b15b5e4a60ddf7285fa0de845c1c0afb730208a65623fa0ede2919827c180ecec7ecde2bd348917f3d3e8a1ca5dd15eddbc828b001e11b9e10cb3fc4cafc2fad40a96d203c1189bed7b167f8d42faca45bcb174d979286751a7ed6d1b7771c1baa4a46171e59940a9ba0cc4e36b77bc48bec0a7a7b89f0a6e762dd085a16d0424045462ef93791cc126a1b45c912312908c09989b50bb61157861e3bb2b9df764daf632341e755f7f000b4c5ce0a0dd0037753092f75e88b41b6dc27f5ae4289310066c3c6d9e393e30be156b71e9e926d2969c30a14130c2d1119be86d5794b4e7a96641cde6278b334b0327dbda3a77939617afa0bbdaee77e7f1d15d12dd4856f8c4f72d799c06afeab1aa5d7570efb7f4ea5c7f3ca4d10dc07b7d98efbabf081903888096db54d5027ed12788f6736a66f86c613ecfed677c3e2de330a2bd2e3eb8b6cc5c9572d661e5cddafef08b79882e6f6cf2728d20ef5197ce2e52aab6be865c23a64bff16a8cc75acfadf0b5980a7645981a83fa9cb23cc41df71200282dade7d3af7b36107ecfec33e23921cf1f8b5ca2b53e37aa9cb5188584dbd51f5e52ca67126e935d66a23ed8196e5ff088b19cda379280bc663ffa039c24a218cb2694c12f59b56a0707c84bb8dc5f77da9b814c47997fdf1ee74d5f726c4fa8905197dd453a7a62dbb9c9e08c3634588d0b94e77dc1f9708766b49ab25e8922b9f69f85f7a2dcba8c4b41d7927079e33064e01953c8d21e055eb64288ffb58d0ec5dbf58a17daa230dce6cdafd728a5611fdef89a09aac05e6d8e16fa340d7469ec7cdc86fc532a73e093261531be2cceca78af2d407a1fcc22409203074c9e443dacb8118a7c11aa221765a63ca81525c34a11d39fe06532b64f73e925c822c6bf873b0d5ea4fca07f895d4c15d5f5e6c7570fb1541e53d3c577e72644325a1ad8140128ed48657643731c3f5bc3f2394685d2f90074098df93cf70cda10cd422ec6bc63a63da1490ae3e230828c1f35641f8a9194adf9e13a021a8ccd7cc92c37d2826e68666afca8ecb39d30cf86718767e9b8ce8e4d4517c9022462047bccade1f7fe1a260563b4aebffbb1ed02d3002d418b563378a65b40db92fa5446dfa0674a4bc446b50c82daeece3da2a25b86f19cb4384a61d3c1d567edb75ed6f6e2a6db354936a84fb2f181b06d370d8c9ffcd9b86e96595534826a4d7b6c822cccb8b64356dbbc4c41351b13144c5c3455225a283ce0cad637a54edc360234be298441f164342a6ec87e63492af9438e017647879827a56a69d43c2d41764cf42ea0e3efa93d4dd909267f5e358e1e5e7c9e81679ee815ddd0e7a3c386651ad54d2fa9070a97ac6be044f5c5e28fc21bb606615a3571166b4a40ea6d5b80c2859c052d9f7d65c56a54a734b07fa93c4f6c3b3ef2806089cc4856a796047b5e963b57670da011a43599b5bc54a3f5da8624fb36cbba095154c04e3175be801289c0b52b9b73e7b0ed558e97470a798e409f7602f407c6d54e45dc75f3796047ca24a4a6e3ebb56a88217e7c00283844ad264271151a699fe82e26cfb8c232b078570485c5ca323d1e45294056fcb2416c762efa3e99b2cbd66a77d0ae6c5c42166800eaf2599e6d9a39da1057003aa5bc893f6678f23225ce820a60615b67fb3eaa756c9a974a60e6934c39bceabd98f8a369b6dae9f1f267bc396bb73924c8648b0cd1304b0592d2c0daef41449d98d1baede7e4ecc27b7a3042dfa96b278b23ae18cbfa9aadb73a2af9a08a5da09e5359710957e3fbe44b2ee31a1312be0c95d1d121ad9932a19bfd12710b86a7f224306fb30335d53c0ca968be0eb1a08397466003e3e184a584e0f3bdc0bdfa27295ef02c6c636e3bef40f70719e353bd603fa3e73c8d731cad6366c0301b569761ac1501688f29b1a5b4d6786d7a96186a123c0e87551aef5b4b96e65f802ddc6450d2d3ff28b530a7bef37a5aa2b73f5485f0e40a6ead2b712a8d15cd94164b00be8947c56e6f5ddc90fd939630d86ad4f6b7801ee626640b02136d9a7d13be1d4eac139e480dbe9e8fca6c1a8285359e62642baccb5a2baf51c7c084a6d91cd2b0527479257678591837197fb9c5c582539ec2856d7b9070eb49c1c5eeb011cec652ea46bf717d07454660ccf884bfe4b5a291e4ac015601398e177b06dcba3041b40455754fbdeb1c6a28db99b9fbf2966f03451d1cb874098a9e3cbdf9aea2b2f8bcd29300e52fcf8aec97f9f2389c4a582f66809f706e31e4fe2259e533d7d305aaea515143fd0054a5f2089960644e6fb2c05509e8ebe532907eea8c3b864bfbf2e7cde9b6845a688423ed64aa1b1bba62edfe0537cf6b1a02b91eef29235672229ddc5b09eb14eb2029ecb6daedea750ef17f66ffc949395508b09f56bd23dfd64da6570c6f9fea9bb5bcc5a89f840def39d94eb719b23970fb003ff184e781e0648d247137a75962ee290355faa8bd09bb6521a202d8451a52ec37d45bdcd4fb6ddf51601af140b0302fe398afe496fd139e2d26579093100697652e17e93580c4d276d9b2f8844a7b18c44d007a82c429054fe09cdd9471f891e5c8852605d19948194fe3386e13d64b2ff9b6e5caa80d21251d94c2782fb04d440dfda38e9ac963290e37d82d700fd97cbace9e8b1943b6ef69a4753485a9e459575d308e289a3c71613c0b41c91d2b9d7fe2a6d347b802b70f0714089ba2bca18be8c593836f1453ad311e307a675a81bc137131d8ea85e49376c7ed33634569e175499d82f45def47ae39ba48c6bbe3a5b340a4e8b5eae25bcf29bf2bce535e17b1b4fd4608d9ff6198b7df29bee77e1815f4dfc7d8bdec7a129f6d6a16534f85d03dcda4b96be76f2543c89c49ae7c3dc7db3af93da966dee1659142920df04b34a09cf11fb64df26ee008ede13aaef8d53597b2cebbd28f49ee2cd91e73f3dedac10e55e77b331ac034665986b51ed71bdca145cb765b8920de42c871020fbb6f82ea491fce01424b4e56e0eb498dc45628eb1da67e2416b813b1beb2bed7e484e88ca627dc312a4746d739b3cfff6dbfdfefc7d73dfd7748f09cedbc4ba81de4040c187108beef02db72b567827e47aa010fbd9157006741a497ec18a966670e5bd826c3133cebbae1989dc21b42802b8688ff587526140552212bda4a3f3b830ff3226a87881e9aeb383a4fa2510acfe9fef64736ae1ddf9bbf2fb7cd141a3868b9b2e77450532b2667c06813bf2290a278d3a393a7522f04cb433a533a52678ee1046a1d2de7534ab7c055fc36cbdbeb4d5192d6a3fc4e640f5e09325725b2a8992e6b9c92dc27d2bd11d598fc6c087c2e198e64d57de242e87a3f68bf96dc846918a401588382fc5570b01d1888dc213c9e4dcb48bde17cc94cf2a60957dab5bb86a318b3b4eabacafc8343810864b4df122150269f34b878a8f81b65b14c68c8338f1edaf491c5fe73921c29f179fa0e74656c0a6d3c240d2b4fe0648979d5c7f97b52d555cc1ee0578cc861c0678715c1c18497a9567329232251d56d012841804aa65f05237c7e023c6a87e88b8dbbaecf18a7352dcea4c8806e6bbb1e96144188b29bfb7aaafa65173c53ad9de1b7fd3814acb5adcafd48a5226359ee19d7b85df24cf9a958e9e4d5d967d663d1673b02b9c124471337ad79188f348f896a262599ea593cd293ee0ebd3bbdc527a080b054cf8b3f2db65b59a7ef6150b87ae0d573ce251f8fb04351f404251aa0e46dfa4977b15ea1d2a55fc43e08d6a8d8b4ddaa4733e16676ef0bf8e001a426436951cebc8e28450e0e9b2aedcb33dc7eb715b8616334bf6060fd5cb51e86daf8426db0d03874cfb60898760aa00beb596bbe7c00bc99b202880363d65462d84c575d026b9e98509c1073c0b220771bd49926be1f21f322c63403b13eda175ed3c4bbeebb9033d6d7c9c73f62b82845f1d3967acc92ae90a89bc4a17603935d77bf49ecc7c045705d6cbbbc56207a5c093b0983d7dca4720445306b0cd709b2ecaaae8f4bb44680bf6ade0fd948bf06b59dd045548e4301a529a297dbe33be32750584715071afa2985a3d4d6f9f7886b72d8a999f80adf18617c4359f3b25e043ce1170bb9099e8439c39d80bb045f7d099d0757f6c7c1f57b7fdfcc971bc505f9823783096a586a71085ac80471e8e32fc65991c8c5901f0c7669b7b7b2437bd32db32625a49aa230d35b377c8a0dbeb745e4479533b2ef9a3e592265cc7d8161f5dbc19adb5d7385f824f229a09f05c5f59bfffd61a12d42d786a1835ce1ed24504801235d0efa8c5d6a13cfddfdb462063a1fa097985138bf96f97fc1b5dabcc98aa13f5804b6c92957b4a576e278a91573b1f8b31b4b87fc6bd73d620ac101d337b09f0f5191df67766fb654309727ff166cd7a69e4f35d12f4648ce381528a3a06856cc6f4505024c24481189f952f73d538c20e6956e39a57b83acc3516e8536d710e58b5005f34820fbd1a2eceecc66050147d89f728bdc52ae1457462b8a68be1af5440e9279d0519c71f57938bea5a8714fe5be07a441569058c7de4876582f306365d9ae7d76df7928a04f60e50aeb184a6655d3a4c2af4732a824b0b4fe185bf844eab81b641324fd42e74fd1cc63270f4cb12375866f4afee1d6f704f21fb1800eb5216a3a3f4a0c37bec700be07d645e1208f3fdd858056be3144652a0cd5946cbb8f4588d9bdf0f398a5e77af913f326145dd5bbc9d055c14761f956c091ed03ebdb4b9dad384acc59d59247ab78fa343b0e2dce8f0c46f85524fa1501ffe03b8f389069c0d062a9fd1006daf837130c1a753228ff47968c2e816ef82cfc4fdc8900ca749993c888efc245da5c484934b2f0bade948f41e2578a90da9ca35df7f0739bf87377acca1830edb23f05f1e0e92f4cfda69e9010933c9f5eaeb2caf8bd721d09f3af98d4022ce1f8de74d02fa9b085e6cd5746a7faf3aa86eb1b53aa263e28ebf9e3259293e1091ffa53859c58f93f5acb65eca49c284992e295ffe2440cc498017865e7bba676b04831cc5f9fa7e979ae0582d26bc19926ee47dcbe927bd973ced32e8704826f3d558780a2bfa01be7168a296c18c6f1dcd401e73ec1c50481495dd90348d6e866e0675589838bac10cb41b4471af353f5aca974fa856ab9185c3c1256af9b3b08475e68a9ce9722f301c5d182b351c041221d11a5f31921097de894fe94935ebc08eb06ee75a94191376ad097740e7c9e79742bd9f7636f273585824628f41c616755072fb9309eeb89dc55eea1f5e6b79bc718bb5545d7aeab52a7f54040030b8f0c5b04ff4f4b33526ba3a5d7e99c9bd133d105815f8efc845c9f4d94e3eeda3f65d426f0547cf6a157d93cdfa332b826bb90e504da164bc3b05f3416e2c3c05e01074e8ab44feac6e305cb45d1c49823c63090bf4e78eaddbd18247b75ac16a187cabb44c3bdb2674796485762f32faf23b23a98731fe8da1b220ee76fdc672e10131f05254cd30019674bd0960e6fb0afd6a10ee87bd249b4233f2486b105291a987831f494dcd43ec2715adae9f77c9346d1643adfb936f2deb2013597a2212f6848ce98ff88dd3b04fe9484aa9c59727d9cfab8e1902be94778eee97f9ad1cb18eddd146c965a1061a56549794b99b82d353033e5564c79ae9111d91856ee5bc00cc66e7a7292bf15552d839799267d119ffb6d07ca1aca83b2f49174060e9b7e1c24357b2f994186b4e16fa3258dbbd137a87c3e1549addc7ca15191c6f6d97b28f53ba3df1893c6cb644422119f40ab50c2b5043108486f35eafb0fb4575d25dd50c38f57c52538e5f1b890dd4ccc59c450debc7c9f623a80962cb75a50cf499d888e9f259a2fee5d7d1ca660eeaf166a8bcf6a4f8c1b0e25d1017d0c1efe57a82cf636112cc864b467325e3e7741fb3dd0d49ee65260c5b8528444605f68f737b47d131e22388a67b6f17b4328f22b28d3f462dd91b7e44919ba42cb60100b79b221837e05c32625ed1cd770ba06a7b9b75332548e850fc0cafa44cf994114fb7d1ae204db9dbe294034f67f3b5ea9da5cf7aff656ddec7d648b6ba827c85a112bc334dd5ad5bba3c624a76fca560d03faff2d963348553ad3aac3f4b9c10abd4cfaaceb80327ce303151cf1c996fc6cd303a9e954bb65c72728fcecb2971a2827f473091b4121f89f40a3f2ac057d7b7d212c43dfac453b3917712df9f22fb83e28c19cda26f658618e44f1ac6cbfd9cd375205a5732de484ff4678eb99db90b7ee7566d7a71bd69de840a42922712d87bc2d78e46db6e10752414ce07b54f8c7507bb533d6a818b58616cb7177f0e3c8a3fe7eb60e7229a021fa8f1f3875c8a6dbfbcc6ff7301c67dcc6450216f7a3383be5905edc6f1c049d8e77095506d19c29688c8197b5b32dfb89df4e56ece376e1ec87c6d26efe95e19969d03353761cea256f44d3517bac57a56c51996ab6d9277490eee379a68303f81dbb0418f5e856df1a2c94ec82f92f53748080e9503edc497554c63ed0c9c4bffa55e0463268d004942843ac6014ddebabea226a7306299afb2e3af4f239b5209f44b6cb220689a2865f2d1d353c63fc80be1b517cf175c614f38e25843472d673f41e4f58a097ea6d0e4f39cab56a46791265e97391ff4d37df5956f97100da61fac962eb277a00dcc49b2619edcdc7f98f61001aa904a783633a42c093e52d5efb9cf47defd3ed80946fc36dd85c67a54c33c98e9a45456a017c9066fc3d416c01c9648fe5ea528151d0d460f265b669fdd46569857788e95049222a13fe131b069a31142178de2393db60e68cb2c8afce0199698d9379f0f9100d0d7400625d7a286d792b627d76ae4b016f97e6b5c04604b79d1b459bcbfa4b9b7d287b1cf58c53af359d60590f1f41fcd25322841e06996681fa1f4d0ef91c5c811f90d2d1570ee84ad32aeb1f8137ab57c3e75b2fa279402b89f4c294b88e271e9b85cda29dd99b49094af8dd9d3ccbfc8be13c45f98a6116f5cd7e49591479cc2c202850dc8a68ed1f00cf7cfae1b5d7f9a06260b19679741e6df0eaeba0af563de3561dc656eebd884080b5e9d64b6302721d3a223484b0c201696cae00da01ce0804f297cc750019107cc8b607c3ccc3e93f13e8fdeb7181338ce189b7d21b4a338ef1ea69f7e04d18c7fa9361e9a6e742f8f2ef457b5580cdc7a754047607bc35fb99f2e18f348fe5b26f7dbff6804ce90d4dd4e076dc2de5979d4fa0ddcffb394acd7813a433a24a540039be581b2307ae360d6fb3bfa0c79df8c0cf9d5fad1f35987729a30aa15acfbd328fb319ca705a99a40296248c91308c878428a7b93e8a2783a4843e898a5dd1932de990614f5089a71eaa43804bd611e9c1c7979242a7c555b2d92bc1dd4703033bd443ddd1dfcdecb6792aa704b4f67f8bd760ce6d9e06527b103deb99eaa92f6754fc039ddec70e1dbe91f7f83eef4803b9088cbcc393589c55b55337be5f9e273781dbae114214db5b52fcbbb5d3475cc8a5751f45cb493f9f8c7b5912b1472bbdb1043e54302f61e3b16a8b26239c420c2f69797e24a2889f0bbe8ec11a1a7075be08721cbc2519abb24aa4fc084f7d3614ef79b373f7be3db78aeb627d4e9ce023b3af30e5f0a0bd9a4a51be0d63974cd0803515a16ef4013eb4c2ceeae27820e833acf65075cdc5bdb53a980b82a3efb17604acb0a18a7088fcd1e513d2f59fb2ff83f42252bfdb63e391e4b1fd46136584af77d32ea6753d61e4d44241772970704c1b9de69d4a7fd23c3e866434b7d3da6c5eb93ba91cbdd9383c68f1a5cd1bb34074a80899b9931763927339678338105457d20fa310ac385e1df2062f44da1c4749ad68fd6ccb152e5c68df6588e64e1874ca66ab0a13f0c0061d45216fa7a961b647683ff68a0b9db2924a293a3ad2fa6010a94b173b3ad2ca678dc302985b39cbff033ced68783111e8fa43ca42b85c13e37b9df9f5c46d6fbb723067223fa7518ef5ea194cc0c061b129bf69843515a55d083b5199bd3c4bf6b40aae41c15f6e522484e9329afb5609e274e22df7b2a91f8f5d81ef4288be8e28d6de92be60649bc33421b8b529795743c6a8d594f93802165767ae73adcf4b83160dc867bb34c2618c509fe89fb63aaa1f8efd4be6d0e009b64b2174f41c9a6e8dd6d7b665da8a0a9b3cfdb00463cef51acc8371c113bda5189dff768219829aac97d3fd86722695788219163ca29c8a87d252f6a5d4c49a1d9e5246eb6dc3cc5bead60b87a0951f2d9c2a81ae05f8149b7451787599126faaaf51ecb391f3afe64469778827d539a91fd19e489e0b37d182e2630b7654b40d4f89a01530a75b6748e030debe0bd8ebe8352ab953b841c19b006a5fc67891071da455823424c261af7b9081901ad734e5a328c6e9ca8c18630fbfd11e710d3c87225f8760b85a95cbbc728cd9c98a7d3e8c3197c16518ab6c328e4ed97b0231865735c3554872c4e357e02a355e922680e9ab19d037f0db40c5a19a8f9b3174983d41aa6f001508012e4e88cd4aeaab15de8ef60683f039c44e6262edead764a16980fbae884b2222fba8f07f18889637d00186f93a3fcfd20cf1db635092da1375bf739b8c291ec152943d0dbb16ffe4b07f6929575f3f3ef56ddd0baf5a963478e32574036feda9b1870626c8b808889b92624b319dfa55e22946aa6a9cc5479160bec03db7af4ba01894a0ac1336e8e67a0cf5a47ef19ef70b6fad28cf9736bcc22e977cf9971b7e9457e9ca9db6a836d0d93c362cc90bd12c28d55f66278f0956a287268d6d5dee72db304a4147dd8bf3512663fb63e7addf51e623d6b3b69bf457d5ae9f299300c85c18d46f371e96a98356c3f61a623b138cf9afd61152db2322b22672f17a82d68677d8d54fda7d3deb9dc8ea5eedee0a4e162cc64c916de9faff84ac178e2151e2dcd301bbbb683209cfa34929d7db880f6c243a3e87478fd177e419fb4cc1d32d0f033dc0c21659c13b71e8a9db8d182e958f3787f486cc724d9ddac9b113f4240854d653043c71dc6d02e47c2745b174f6c03395b9b2cf0f025b57e9a3dc4e971192c95a5595af4b1a6df693e11a5036cb37732f08575d5fd848401f77419e272fa6ed1493479c46d6f8a8e59b5a0e45f75191247ddadbd5a2caea6e0522ce89606d76a8cc2f89ccb3b2b4c496eb8f115c10996ec6471f9a8b170a129e8a34727a99e73758496f1c917a66da320ad56763f9c49a219dbde53fc702777597bf73cf02c7165dbc686513fa91826a14d27e827de8e13ba33d30734b9a5e9fe082299fedd0df7aa457c8ead1c3289321d802a1411f0bd1094f37609a431e435cb12d2a488c0cd9adc496f253f43f055a2d3dcb46dc198088eaec750ce56123f3d05b2ec08430caecb21f4900db7ff5170f87c78417fa5c9465058442c4960993ed4263f1a7888f3c610d1eb1fc3ca09ce5b00957deeb5c6e58bb95d8277d226b4ce2273c2b955b59db66b8705163b4ad3df37fc04c4e9f90d902e8cc099e29259138142643303aaea5098f2a9ba8c666f59891466af91cc5ddaaff0cdcd1c6a2008fb115bd768b021553586cd7668390356ee659df8776969e4f573c9850b7c974ac6668b14bf34e280bcb87b3ac134e912ed4e9316160efe988564b5195c34bb31b49d1995777eea0d4f1654eb479f6ca0312621a6390c9bf731033526bfa4f7290c77b94b547dd3c7aea4e88402b970b7b1042b0a50583914a016e2d6a991fd06402e615396532235bb66be07011ec77144f3e93e2274b0b4d3770aa7c2e59d3a2d971f960a90c5e3baaf13c395b74445bdb1ef6679ae3ca2cb7c56c4387212aa4ea12ed1c6891602b569488025cece2072d2a2b839b1548ae1f5911877542212b346782e58e3e7ee7abadc660bbbab5f63708807ec7bb177dbaff7ee0aa635dc83591fb2046c92a6e99b1d13cf2c9dfd427b468cb13dc9078ffe72ff6165d5e773ff45de787e964f811514d835d3671da550d592ceb055d927475ae194ce0f5325203182658b3e19cbad8452cbacb327fa903ff0399806f729fd8d60c61cdd20d9ff847b9913f40de79ae9a1f5e3eb77274b4c3103cf5336b65b7e0c31f56a20b8b55d4aa125614c0e0ed422ee3bf9f45fd63437447f04667e9f41da4b1c95c7529b2c4bb0b599afda17c0b40b8d37814b8c9600646e33968efa656a4a78dd38fb307bb343f53e3121851ad9613832e34eb28673bf63beb506f48d0161b6de73674103bd382c341f5159982a154da8fcccd77f3f49a7eb83aef872b02fc863dbc77e0ce33677f0d6cfea8326c45478e9e5f5b6d912cd57aac4b67179c60c1dcea7950933a43f456a7bd5b768f32170e92ffe214c5059111027362927d21ab8fb70191c6ba0157c18e974be3c2084e4734af42d92d8e8d630d66cffa6087fe4a07cc0e5a5292c68ae895ebd4c60ce877d17dc7669d02a6b6dcdaf180dfeb525cb236ddaf34bebb0dcd694a8e1c3a726fe18d0379ca6b870d26c1d86f4be11ee0739e937da0e09a5b12475bf8d310a09b200e3392ec6be29d5cb6697b70745749ac8bc3a094d2d1dbc609b63b84240c2b39bf0d9f0f03438fcee42033bd826d4d7fa80bea21a05ed59fa0c9742dc8b47311156da9f656b0ce985dd62810998fb7f70aff54c9f157e9be138db6b9d68a03ba596e18587b3b789a7bc39738f3606227bbadceae90afe72bda93a8bdba8590f63f0b1da0570e6ba6d32711d07b806cd16261b0c8d177d37ccaa8fefb4abba232837f446ea1a34b07cb467efd0e3ba40f0c88ecf05cdf68fefa15902c834f4280070699548552d633923f46cfebc238581475d9b8f947a6f18884786abdb309d2588a8c473171cb546e355651da78c943e4e0934f734e247b5d69cd899c76c43ef0b41b3330f3ada15e861059396bc6c42b4ca3a539c30044fabc4bc2f4594864ac38663a527bb14c114ac39673657bc58f67041b8972cd686e56a026689f46c83e538111158e3051aac8246542a993c471e44e79e28f551fb78b0e15d1a1db8aa7a68073165c3057054b5292e6b76c41b9e52ecf4e35032ffd51f7c8963e2467a2d433c22c15730b43df898ee48bd7b784e2184d6feb85c71661bac46bcfcdaa47ed33fcff7e86aafd80ef004f651fa0e75a776a94ae0c98f6f3bc27c89336dddcbab18614826a9f7e2dafa8f462dfef8f7df33d8093c0ddf1b29e9537dd08c12594ae203ad838b1cdeb7b44568540f04ba88022ea03f07e406d24147fde59495437fe731fca790fce2c026196f1e4745922c2d0988932914590bc8c4ca02d4eaf48aab2d77701d3fa1323cf0a910635c30f05d6947007262f97b6d4e85adc7a6caf5eecaef8dd03d1f46e6433865cfbce7bf23056056a5fd2f5002dc83844a00e76fdc66adaadf3388692ce5c1cb9e426c765df0735baa0cf175b092e4a3c7836759369745bed106379c36911b2319e83a935a12507ab535a446a178b03277467d0e445437115409727732056dc5a31f6eeb1b24e4d533256bb054ce0b395d5b58915b0c559c492fc22d061734aa9a794f0d67f58684690479404fc57d4dd2c5851bb58160da526b2512b218ea13cfa594af5287b4b6e4683702cc6d48b66a176ec00a4a850e61ad120f33f9d21d3fbf62c1c98e7142d7956201c938d71b8abfff66dbc4c7b83e13bd6d7645a16bccd3eb7b370870be82dd9eaca4f8b4aff9d3bcd4762e448dc9d2d8c706f818405000e94db55125bb704946cc559fe203d437cbcd844799c685b71e3bcaa786a22ea9bbd27a6d796e15b4d26a51f619d27bac3938d261b4207ddb4e4407437d0a48550af3df94d4b5ed37fd2ea1f10c6179015429ee222ad1d465cd16c0003d41fcfcfcd7aaef1c7ae6acf782a2a6fdb5e6e855e7ddc44362289826031cad369bcfe01be09d48a42356dde3805288300a80b236180bf4b1795bdcf76089c12203a69cd31ec280b946a39dbb3a595bf4e5087b7db32f8cc378c2c01de9bcf44cd87d0b48bbf9975b13bb0c102073eda167f2bc3d26e0dfc865f8cc4c71e39938b2de9bafbf91ed46a04b5dbbb2de1090bc0b6a3508889dd1e2ef3e9e0f3a4011b02f4f2af7749bf81df34624e9f0cf2c1400a734397dee6d7fc15c0e36d29589397149802ab4a4a36ffa0ad6413edb92f68326fdc8d59e3653e0b6395a6eaebcab22e99f676fa6dc06214419f41671bd60ce6e352b6b04a20a861f70f62c307368c01bc75b5d48cc6e6e74f6e30751cfb063cb4f302df0de09698b2a18ce13de16f6d6c1ed7f03aac1eabfd5548d6f54a75f864163273e7b61cd686315be4181c84869e527268d812c89800a9b5e6d51d11bfd6436e4935b2ab3b532adaef2c49115ca34cf300a94889a15622fbf00ddfb955462c5b0c1632c7494bf8491b02736e6ad2aaedf7ab32a50a95e3a34d437846ae6c92876bd2f2ca489d9dc29a053595ecf345ed813cc2a9436bfb3125ea1fef4adb54035ebc8c4fbd891890e90ca00820b01a983ccb46858fe287c6980ce6bcadbbb9cedb2566c7cf0f663958ec76e73e9ba31355ba90b8f376c259f6de420137bf4f46620d56392c81442e45bb60a0c80936e4c636d5cab45a9d28a85ad57ae5f66688a1181a1a1439e4c177c4cab7596e736efe67d4171716d68dcda90d087c8733edd5c66d097437f47ee21465ebc5ba25ad77c083d5d8c1e13e2ce595c9919910400cc84ec69742d3e5c606a5d73871a8b6c4a38f961e7717374aa3920313e11186376656e44b1de12d53cc04fb06aa0805d780cb449f2f31b1f7df433f06d13aabfd488a113a4fe84616ae41ab2a38017c6c7bc7e236a1ef3bdc2a8cf8202ddeae5324a6f07f7f1f8efb6d146e54a9092ddaa5e3f3f2ae1556ea1995597f541069ab581ab75da7f03ddf784d20e850c02ffaead52b0fa7ec73022319e2e9a7d5355e15c921a44292c6f6412cc8cf88567ec69de3afca3d7377599b30b653f74efe0b1f855ed430ca787e3be6c89f501a93e0a8e7556261166b3b4a5310f2b8ee145403a097b7a06ee293bd5edaa7c66ab636a2fd2cf1457ce52f854662ca67b095ff33e6ac25ba8bc4939484691d372a4c107dcbf60825b91c41dc76358df7aec3445de5bb16dd95039911bb52f132e91f83d2995a3059c5f3bcffa53751dd9d54edb6d066da3840d500e65bf3da78648ceec3d566dc9761f30af038e04d734f0eaaad3436cb4270da9c5b98ce38a57a754c9263efb3e4c48e90ded677805e7216ff6e5620612eec2714c1a3d225f22269a9bd71b9fb5cfb09b8b71ccee872bb42d6977f26feea40531a7aea1e3f4d130beb7ce2b63b85bca7a0f5638cf578075d15f0ad0f399893016625d3abe4a0a1adf29b4b15bded8875c7ce614e2196c0fe5ef1ede8baad707d4a82f77636f767aae9eb007567ec5e99aff4d9a33b4f8118bd257bd5337372f47826bb67851b567ff4ce1647b91a61f99abea40949575c519d95c820f5de6fc30e1ef6d4f4f81e173033c4baf89ee0b3d3dea2a03706e0d6f95a514c29939bb717fb9203d503eb1956bb1f819e049294801b0474085339047d572bda86085ac74ec429404b019ec9ac5736790e7ea88b1f92b8e4a2fec2498f0871783a29cc476756ba96ac844519a52f1ce0bfca80b10dc4482fe5b90e9766d5150670d08e05a66cc604839420772d29962d9fdbdc7245d2f82218a858f03becb1c889cc5311201e2990f07822cecfdaade447c43607994e9aa5539e101bf4fd6f1d3b4c84a40642a3dcbb417791d14aff51a93132c2c7f6c075d47660130559c7e6af04664ed0196a44f7529f412701963d27b6936d90d9146dd7d481d7da60db6e49109186e64d150aff1dc0047ac807f3a7116ac476d98db38f8ced67e123880385dc5f6611b337a811fa55b24854f8fb642ae927849c7b224296fe471f68fe484088e2aea27837afa387f869a470143fcd58891af7e150645dc690017cd6d025b02aff9c4f4665480ffdd8e3420382d72d664cb51ac6e3eedc0e51c187db896a5cfb16ac31479153808d6604ee548910f7e2d2dc2de584ea64b99bea62d5c84d30a94fbcdae4857e3bf52bd139a8b18904959596250c880bd943d35ac7527793766f2ef4b1c366a08e086c064260b45d15d13a1fb9c64c9821e36ea7bd3aba5f2985bd0941e3706cb38525203e5f96fbb3686f390febc3ee6455c6c8f5c6f44c396c815ec6138abf56622d5161464ef21244a4e0410f9b9706f2f5a42c835e9f340148cb149cd8af9a24bf93eead644589fa6864745c2ba06dbb9cd195f716ab2280874192fd6994cd9ca08b63beaeec16223f091e4b4a69cd035bf4dbe50c14b1e297b87425951c878b5864b3652a8729a427c459ace4f7a358b77e7a5d52c2d2e517a28bb42ac837bc2884ce1884269f5b46f41a78ea423bb66bc0a5e4e1a72fa52b9a4e0bc2dd744429f58df2128bc2380cf3a364d1f865355efc5ea44a8c960ce95a4a6f93c82b97f59e2a3ec7252506b9f0d95a014cdded4a4429ae1cccd9bd6454d7ed4be02e2f22b396c54ff35c112ef42ad94ee27ee88b9ebe31a50727fa347b7867d62c6d2100d954140380286f34f02dab5dcf0f191e92e671bca78f1a8b6bf0e84cfc3c6c65694ab5ba84eda96209ba665672685bbf83cc6e2890b6a60b813836954ba142b017f0fca3b4eeeaafe894a78cff3e49b84ba1dfa71c60742d152bbe21f5d027f28cd48fc6f69b3723071957ff0494755cc834df0229edf5f7ca8123c70c826dd86570ed2804001d1d036a1a8759fa6a7965ff5e49cd3ff9b06cfaa442bf3c23d2401a2e68cd866edcc83631f9616c849487f5caba9df4a6e0477f03b51fdf46943926cd6dbed5634b5d4e9491a88d37758f938f66649bb7a04fc92ffca230ebe58e1b0bd3216f508cbb04cdafc93f9b241aaec22d2b9c04f20a291b5f226bd8da69dc1c5d8b1856ce8e5029dfdfbf2d22b683b3a43e1c8610fc11896d59015b7b8dfd9dea072f0668eec17a3178f72982318666b679ccd113214c22905daa6a9a8d4fe70a5a24a96888fcf22189832e3fc387baec56ccb6b81fc0c32315acce24dceec5d7edfbeb4956a8dee9fea3a8dc0cbf3732d32d19b872322fa4a5d46a0616305ef0db4a17304f4f53f5528dd2d97b6979baebc01446558bdbf5759c64f087279e918b48f6e669360cf091f918c3b4403b9adf886514e061f210b392a4cd6ff2474ceb26a9dea15450a2f13644ced968bdb6f2ee660a63d31e857fbdfdff01e2b9b4c549be7af8f2e9aa89017af73c8d911b5603612aa0deb7a97aefdcd8921ffcb8a4a506fd3cc6c813cb0812c93506f36422a50df3f4453c961535fa4da19a81e93f167cd9e5635b60e792b39eb53daf0235f536547de9793ac3c8e66b87e8bc7b30ac7b09b4e6f5ad6d0e710a27a6323f127fe304e8a300a339746cbdf6eccdd240d42312b9622942db0e575b5ad231112f22fa138439e4c9298239c3109150f69d142cdda9eaa4be6e4a43b0ad48b4a251604bc172d4c1289471ab35cac85b44271ff771a7bf10ea49e3b605464ba2ddc643f79e592a654d575d5bea839d81dcb83ebd61ae541e85581abab2ac3a2c04742f1e154c0435153ca673fbf115853dbf67ca9e485e1e3e24f0824ed6af1e7ce8e3f25911930268c32685e6ee27717840f53c99d07517dd2683bcb691ea839124fa5b36c34cc8abaa8647996981c17a625b7ebe03d92bfc5c3d708c72ff2b741c47184172f2ad5b4be5c380e89e12f00ebd231a834540a6ee2ab4355ac37b447ab72202e7334f2fb97217ee75579178acd51b8492e6c6d0c333607a444acf21a8629094e49993202b5533615d0720177063603697dcb63f80419947c484c32ab25fee29cb76272721fbf743680782d74d77f7678a35a535c24440ccbee96533d66e0e44821935292164e0657047be044fc97b5ec77c5b7282bf888143f37421215285edf0df70b50b76df8ef9ca00c68548a0823f72cd7cb00fa6a78636fdcae60b7a3dc187a46636943571175a2899f27d4e5bef64caadfca7c27b41f0a41c73a832eaa4c127004012d22376aa1dec2f0ea39bf453178055777e79f752ec9c2e5f09f91ce51bac42d8c6e70d3cb66485044fb039d58dc7dfad39a20f87d84e6abba2977d38385b5e2f9c4a47cbd1416f495f496ca8fe334a1d01e410eb8fa84deaa1e0df5af880b371810b849b3c8ceba33550222e81cfd337f8b4e27607b0b45b11fba3a2c3de75fc738626fb181bf0ddc2dfc657a07e4ab4aba2741e1b0dfdc55a93335196a0e8a2dbe1de205373b92ee3a9a8ff89a14b63bb97d4eef48f342dd5864ee7a6d1e2982fd50a5cfd25d3680bbf337080f1ac5995db3b4c348139a11b86075b73037ba1c476afa23ecc36bdb2b9d7ae5cd0168f655c175c1623c35cd01f2e6e7481108e1ab4f7bbbe2c80144f6337631e4d011454f59fc024dcf2f40ffc0a352763990e6ccf5050ad772f0bb984244ce2d620838935998a5afdd8afdb7c3427ba4f1813536ea4b06437eda54b006edd2258a8faf2453a7ad9117ffab5a25166bf9c1214e2f50a13860446133aff8c918e0438e7e2b66c9f618567b227c296afccfddd32469c92d2e7d8fabc72ea26acdf5965fbd5a23c18bd14a416b60d3f8424904898993334cc2cf68e8bbd107ad9ef2113c7c6ae71100e91d53e4443bb97eb99de9bae9c4bc335cd1d1b7ad2aecacdfb5e0d0f1d26e6ef62ef2f42128c77f79c4177cf0a6b35cd7ea7a0274d951ba267bc15216a179811ad91f5711a3c06d93536d426749283e0f11c6ad0dbe0e000ce7ac3c957d9e065bf6a8d498fd1b1c56561c3a7d9d8cd8f12090b822c0aa5a475dff5ae2c8ba46f4cc5212d53210424617b1864c8e6fe6bf29fcae4748da6a2ff708b6ae90de5fda224837474facd859293f01ddb6c6508b5210593cac0c6dfaf28aa32bc648e19a1b12d0a94fea75c9c66810971cf35a6216e0929f5c61e31cf4033997885e150e0b047e7905a7e3408058d25a2985fe5e779d3011eb322c021df28ce91ae1d76e4fa05466a9842161f269128af6f3439dc37216cc16ab7f7aabdf4011a4cff99aef54129364ca9b5ca134a57c582e7389d3c35b36c54f05723a5a438884e4f514765762e087d346db91baec006b5a38205587a8612b620a7e44e2e833587700382df0235d239b0d2bd63c3b60951df8c60faa8f89ea3f11b136160333b7becee494298d53ed3a608ffd388c84925d0077922631658361d9066578b1c332b81989aa01b6e023939aca64f92ec97a9aa479740c37708646d5ad3b7bcbdf2db85cf1b9c7adf70f557f850166c7e692ecc2196bc825f7d66ef4a04303ce49e199d69ad2c626f41951b11ead8b50a0fc7597a862fd396632133a570e650e80a0d5f66e674eb78a593b4440660e3375d3f9945e08eb673398f40374e06b597ada6e682d0a590d315aa6f37e68c0db49e93289de60e53cf70b2f07fc71a0320a5eee1db12013108d375c0eedcf42c1dc16b9172dcf94919868ed12a3c1a79691591160bbaac3193ae1e5cb447ec376e7d6bf4c3696027045333833ba3d840d64c280ce83b4b4e361f9ff12397d7d15be3658605fa75a33a2e669f0bca011ed4e2ead6f280caaea858c041a54ea1708526d594c93fd6f8141d54c9a31e6a178d25bc1f09aff5acf7c3536e9f6a76c8adf65f399d4749b8eb5a1d238196b93867be8113e23f0d8b292d2a88adf0201fa7dd583779ebf663b3f6a151c399ac28068a796b4fd5ac7856b605fd6376ae92c8ca66becab43e9a16a8df86b037bbce4cd57e278ac6a582612d84bfd84e7365ee484218d5931214d1a48b94397a45d889a37aa333f5f2064722cff01dd347d529b5c9b7ce1ff4e4c085f14cb2b4da2a7ad020c56c15386a3879fba736f158d947dec2b9cfae57d4083774329dff32b18bedf26d353cae30bf93d907219329bcee8e8a4772fb0cc1067c53a0e038b1f397ea64b1fe3fd9212dcb3e69e08ce7ba8d2486c998b16407ec215566f965376a93e180f22f8808c44c24e4beee18fc7a145340f28c4132be4be614cfe1f151915cd58816e2b62e1fff68f7c8f227586b41bda86a23b065a7513464aac6542187c6b07f332b2a961618381cce2568536f23c59b60edc0a3f2602855c7a6bf5c1e9219f3dda1c12b58f7c0ce4911c18fa6ce57431d4977c8e0f77ec96e103eeb101b8d9c73cf2bae530cfcbc2d3f155d1f2e1ce1c8a0cda110a7523043fc6c4a7a793c931f83e30381cf0da86de8e409ad894851857dc1289eb2d12451ce0d4d595009f7a4d08e628778a2457fa21cf721cc4684306091049c34cf0be3104e994abb809c2e3091870ab4a14dd51defedec0286ff722a3ef3f99758704729297b1fc211a9539f9b7c3bb43000cb56399abd8e447777e19cdd4e4c40ad6a85d37301f3fa6e19d0596bd9331b98e7a1e5abb849f965301521081e827dfa9ffeeb62a2011ae9458a79d0a7ef4fe2082c9fe9a5ec0a08f0f0c7ce96c7f252ee8307ad47a106ff3274dc7955882a6518785204df8c5c803c7c6eefe9da4eaaeaab6e9341fe0a4cc8f4cea2735af7dda023cd2578dc860d4d0e6591d8411b141045d4ce4c3342a5370ab1d42805beacd7894cd5f6a6a8d5cfbca5a1bb141d1da8a3ea9cd3ae2461daf71a77afd494ed5c5425dddc69571a4c47f2d489616cf3ec8f99256128f7f5b1c5f060a8ca3696b091f3a44393808bb8ff63756e0f76bb026e5a04daee8ef4ccae99cfdb609da54955272958b1c9ba63ef2d540621bf82b2dd6968e4c6921fc0a291d62ee87528f4e9caa75a41596899423fdc0af2236203f5ed52640cc5f9c70668e3b9065a44bdd908c66d27bbb8e79b59a3669fc6b8a3d2d940417476bad57161c45eb72052a46f12716365fe23829d91ad60a87dfaac075ff7997c2cc9c7b2de47be1dc61486a74f221da0af3e165d7fb07f09dd07f52762c21988b364d8bddc58dad6a42d0729139c16a8e8acfa81bb0ac9240ba4806fc7d0edff31e53f98ef0d0a92be9b408f5cd816366d86edaa9fdc3b59f5337460ec370ac198ed5b0e0750aa8fdedde77f2e51d46c9c08d73823a9b884d99104d40cc44533f83cee3695a8b4a3b778287bedd8098623c5755037b88fc1d4019a2aeb3c5b021aed65a0493793aca938a1a56684dba715f6c587a469ce7043e0d64670a2c9f48147921e01aebef0277bab21904089f69731f03bfc1f36fd9e95b295c5a57db6cf0810aa158de3a040c97137f5e84ccc202d614cb3f9b3d414f17083f72576723fb7658bec5c87a67d954189a43a5897dbaae55ede79438caaa543abcc70808222e7952a03a30e9468c31af05895634349528fede39a53f30f88fb949df453e62f8acde8b693ab3a103e181ce004079d0b68792c786f82c9ab4ea340d77dc93ee97cfde9de437b62d53657491ee763a0b52612beefd2b495dd9ed63169b700e098a9fd613ce5f39421d7de9c6164b1117c1b69cb02b4a2005f45f6a0a80f0ada238b6236f65ac53928c59c0a9c05ad57cb61fb22333db6acea9ff6d7297d2070517087c9bdf223dfed2e9dc53f2c73b9d1812de50bd61e529cec678d79b137b2fb0b3dbe78082581cd43f4493a2495be2baecf0fee77343345781c40ab0b9533385a2dbcd21086af1d02ecb21cbe9de81a0b085b6b4de38db820d2e887f1f52e2c2b84bcdc86f59360d66ad9a7e04a2f8af525a50acc361905f280345123c6ae38e7f9188d492f80a5aa6e264850d04a22b8624cbfca23e46cc41d90da96f6ba4a379806084d196d0ec187dc8bc49b3746d37501713cdc407db05fcc4da138bba3c6ceac07274211085474bf6625e2120a76a6d62ced076d48f2c0ec1db2ccd3d1c98c1d2fd0370aa8c44ee113d11cc97233872aa1f7e49a8c52740fb587e662c44a1dac3a22b4ec8a3e56002a98e7fd69705c685b76050cf3572cca4d3300fd17c684d9fa02859d9c9eff80bedada5f20a541ada21d8ffb03b4e077a168707e8d3fe6f0de3a844cad5ecfafa392bcfe81b0f4f93f26a5721b2c7cbfd07164a473f32b227d51fa158cd6cd0dcfc45dcdda7159babdb4884f9cb7edd7780eb71d20daddb3eebe684dba0ddee798ecd9ccfd7b6194e55ab366d8879f8d32c340f1fd86e599920ef075dfd9d4d890f7e6f55b1d80467dc8a9022f8e7c4ead63477d1be7f58020dc60cdd5c8962e6cf122f08cc9b5c01cc852fb9095571e207b3154067ca5f8c0669586338bcccb489ed626550c900a9497fa768adb23e99135f74615adeb81fb50f25ea4158fa1569bd75f41f7241eb1542ffdb0df99a5cc42aa18c1520f45c497b090bf2139111bdb4cedd51396059b2290a9c115b090a55552e3ba963da6d9693c7ed708b13978d7179a38a0f27d6a1e5d78aa796fac4452700968acf8ce037ea40029c2b13463a4bdc83ad1fd6f6b1ef3aa4bc8b141e6b0c7d57b9fe2f34b3d67c4d176871d0e8040463b4d4f63cfd29d0a48cb56d4f13a81e629912037565129f0ef6bc80494a9f0b35524e8f214fe5ac265ba250f2906f31eeb3236f73b1bfd91b2461801f89336d21a403723010c5c57e22b6919a10b659b38d6c7734448a5365446e99716031fea245983c67ff6f69923117412f35a445699fc680504ef9c9a57834358e5fa00f4585134401ee14846696c69ac3c291f3ba8a8d8de2ed1f04e38b0ac9f9182ef269ae6af3623d1007010b78ff976222529fb379388a880e438a7a34c02d7eb2c5c740d35e580c53beb1505a738f3a9879cd4c1de43ac4f601a204567bd9c1e562657c764df098bc92439acf2eaaa74a1ad896cd6259b66f51f5e2c2c54a390c3664cee9cc76fb146d107abe383f26ab86140735a418b856ddb5056a2c9d30e79454163ce393800a3ccf4a47cd9d86356abaa7c55a8f2f7baa5ccab51faffb435b971f3765e51fd15fc52d656e1f652e69dcd5cb1c025fb567623f78d507b6de29aa0a855f425be2b2fb95ba32f2f66efa46e9b549ada8cd50eeb87577b4fa1b42a501d9f38ea2646c93d87cc5eadb637515211b20d5627dd4a9da2fc1f10fad0a400f050b0ba3dece66ef96612be89eb0f4bd0aa96616a69cab51c00fa68e845d77b115ecc559b3f4549029c6cd24ae83cdd3e73b64e957c7f87fd7c6385dabe91f77050526e0be893cfc943dfa697c0b9a3e1056425d74917678f020d674e743a1696e4b47a258bebf7e873b92f48f2f5ed36c3669f95cfd168f12b2911ebe501c317d5ca1776215eb3220b40058eddadab1e26f1e4ec47bcaa453d857387c7f910aa3e2b760545d7e49f296a4b59fbd182865cffc251ec1102888c24f9893766db3bf2949dbe112466246cfb4288d95b67972d690c2c72d3a75307211301e9b88b4cecda79a560425a655d69c702419ac7a2cf552161225f5d12bfb3ec7b00bfe98190f21dfb0e17dc1b6aa7b49fa2a190f4a5d1c2853e11d4f72f325023d2424e678efd9a532de7c86ff70eb07bb5bc1d139a9c00cc21f644819c812ab5909ca71da175b9afc6c6442f18d665c63c9f82740aa3640361c2dce07c61ba643dacd525f9922df610c3cb1dbb9a5884ca7b95b24e4e09037004ca93fddddca5882b594db933e5a82d226f4f6f389088fcdbad881744960865498bbfbb208c959bf1f761814a41d20ab29e2ee426d224e7a268baf67693b70619ca4e92bbd6c1c0f4823db4b8b448c96d365e0bd8e6a72536c228df2cfdc714b10448f345ecae4143aac5c81d9c387d4d6cc8299c21a6e88e2214f0735a976ada3a7c3c0e99e1fb7eabe183021a1d98c073e4d2bc39c8d037db2915cc7659c35523a9bdc456fef57845135e510681f4312c4f3ef26484cbc9e1c633d41f507f2a2abca78780c70ff03fb1beb8279d6d6851b3396a44c19b369d32d76f89c2958d2740a8aa861a3adc1622459d9b676e53e70cec93cb290ea217c08519e49d2781b09d6a7db7ebd5901dba57c037ebc6d44a2dcf786d6630c0f06eb2bb7397c81a384b0d59423006cea7461fb60674916a2ce685e0f9b8d193d7096160079c1e83c3cbb858c9ae4b488f7a20f9f437289226677f2972e6de14072299ce42c3e855600032a0306ecddef6d12ee6dbdafa9a178d7a0ce46007e05b6950bcc7399dc59b925e4cc685f3b986d62bd3e8c98bcf35d6b73e209e3bdf9534c2b08c862e9fcbc3bf41d6000879f40525a801b5733ca52f34cb952fc89d1b7612ec426b6215698939948f104a2754aa68e23b074691e6928c9e389b483fa7cc84255ae94fb3d2e3a57b5837ecb0802bf1365ca6a462879f37f5a6757a939fa88c7ae64227bbd96e7ca4fcc69fd82c9551f8a5cfd3b77fd44db02270e88a97da4bbcc05c0815b861f22383a182a661109deddfc96bcd0929721a10936117bcf7663903fe4f3ff0a93775fdd380340c744e3ef942328b973ce8fc5e4f53aa238c24659fe8aceb67d32e6a4898f83bd1addacf7da1574bcd154e7f32df1f3ca6da9b472b9b688eea8b3a06ca62bf6f1d9f4bb495f0243d3a1b8ecb349c2a020a91330a339bc5f7d6097908e38ef32ca5a280d711918b5ce912b05c9d5d82e32cde1e14931299fec3166f092e0390a0eaa8859df2fce3df3b2d44f6d794abac01d55bf8d18f984a893a9611271b1ed2825d8a82044af725c70ca91dbcb906e4d440436f64aca42db22ffda6ba51948a9d1a555cd17360d2a90323d60200a444b0a377261d91b0ec84ddb29db5ba0805d0ec31627e6af719eb4177c7ffdd383c8adf667728d0f0b69cef1f32f0850c620f3be43c4603a37144b01f7bb26f879aa9fe68847ec698f7f8dc477d70c0c2b5f4725a83dfe14c38cf787443d02d9888becd115f701bebf7386f849062f6573c8f68b021540d3016fd7717e605fe28841f8532f1c01ed819bbd84a128dc99bb89599b6e452291333efeb7061f9b118c66810edc3e6c2b448edf61dfb5a6d4cdf77995cac5adecbef3abce0cb20427d1d306efebbee8f38fb5b3a5f30a7e7f997cb20c045f7fde452e7ca10708604ed8690c2a1f2487a53f94d5876527d2b54051b46eefd16673d36209aaf315223e5510f6e82ff64aecbe44edb8057df0f35b1c7f2ebebfd3dc7b483f94cc991d4262892927683dd96e8697954c6c0f38de5c1b24f5f4a37e08fbb75c0fbb35ce6576cec7f55e31e590be1513e193d3d03f44a630e7e109dcb3c7d67ababa729f1d21562072f09a07e002725874c08ca3684284b34eb0e2ffe1118028de6e5fd99f842f43ce8e8530d4951337d1bc2688dd22c4f01e03267ef13bde7c44a376095f306cbca1c392aa8244a6645c92a6eefb6c5e21a6753a2e3c91aea503052112b6904d8841a001f7a260bee22392518d08eaa5f025095c763211e6806886a852faeeb06b4e07b5c952939f6711631759630e0ed5da965d6d14ef3b52f99387eb047eb96c8fc8f7e5b746f260fcb2db714d03dcf4bff3faae7643c3c5daaef2715e1d51458c339b96d300dcd86d2e732dc663cedebd80361ff7e6230550d2d1ae0e9c0552d727ea05fada012c96693dbfca5eb908d6050e8cbd7ef53b793325ed2fab47497816f8833802a550974288f9a162561481a6094b7808ec83f59f314f0a83c70861ab4134163f845a8bf81356b4a25ccf861653b040679f050afe8a227abdee2cd5b915529e248898c09b17dbf5673efcfa67a7e78d7dc40adbf3e5a25fbede096a0f9cc7491526f0f1f230feae7dbc8ffedad07e36d5069f89926edaceefd9c910c6b731080efe600c03d080ec821854a7a54f65dd2ea404c52319ecdca586bf294925c4c905bcfeb60cf7d475d9480f4dda3272e9a314b322e5f30a4b6f0dbae4ac3829c009b60b1353c30d5e81906e9e700ad21198d268517883f2e14bea7d215b38f194fca1a2536903821191ff77df9d93aa860ade97cbec69482cd62d36547790f9590d1df14068ff059d58286647f4d42246379be1cdaa53a3523ba71021b962d9d2e590a3d54d66a6a5c48348f79595afa520f8504d77115fdc4647742bf14a6f0bdc001465bb57516d1954906f89db5e7c8a1e81683efe61f735a9b2ee31f7bb45372049139c680b4049c495810d128511f4c87b37d49b90a519d62e575df2f9ed3072a78c93c1a85a53d60b86ed1a0d2548d5e568603e9de8cb827e03b31f2f1e1b519767f2f72385ed349fd35d7faa93dc7fbca5264a134f3436a2fc81afea9c529dfdeee7107a84f2fc5a946e963c509aad518429b8772a3e2451d2d3f19a38092b5935e605cbc775decf29f5a65e23c68e254334b919513b1014bee3f133bccae3f7e25dff7e821bd27a6e182af4ff9b94351a468ed055eb6bd26ec398fd0776b7f59c344e5c3a901203d4239aa4d3fdae14c88574b7469c5950d2fde79e000a720ed0c61f12627de35631ab9354fd2ca46042e9f9db48603c41f4b1e8cc22fad9341d2b8c29dbf28b487768186c6b4c91cef63a10c2ea14529858a562618480d673395e342312ef99ad2e02085897171ecbe9fb46bafde70892fd488624e21100dd13c65d87c771cf11fc2f8ec873dfb99babb6d1046f27437a3284202c2043a6a2d73b721d6a58a98bc8f9a4ff0a8422a9747b2d1e752bb60654c6015ab2487fd81fa12f7b42c3161d8915181f695b1f71b0b486e06443adc664667141d768d14f202cc3069df4a8875487805835e157c8a4128d8966df65e3d4edceb7ae0c2957a0be3ac982f2a0c0b6ba7e2b078d323ce47c2c281c269abac146c7ec1a1a3ad92a08c9c9b097b23f0644baac8a8f812992c08dba4c4818ee04ccd93518055f953cc2055d8f4bd05df5b069c371f6ab28634d66935e5094de64cf162849e3fb6a62b963101defd1c84d237a010e20ca23f595e95c3680f701525827b8ff7750e60c5d9f5e04efaa0484c1ebde011f74931c1929530a7b4b2e948b8aa95fc845c9220744c464dc719d73f1e545ac76ef1e8bcfaf6f310ce2283aed239a44a350ea4adbd2f77f3c4126c27583525db1d370a08674f9705da3b6ade6903ac4aa920e8a18f7fd11aea5de8c9f95568e1f4bb1a0943580cc6fabad8959d614af6c90b93362aaf1d07e86d1a46b30126c17b4e7c50dabb42d31e8bec5e9e93a90e3fad194d37eb07c38f726728ffbfb5d44e8018f868e00151f7964eb5a218c949f9b8fbc2cce161febf08e4a5bb75714eb9fa9fecdd6b08b1822d2d4f763b27a11b93bda45433614542ce5c516befb254e79f22e69d0d34865ff5a0cf68fadfbf096841dd10ef0c57ac8231de2158ba6737629f02ea3a99c78c867c2489d36a50a2eea9abc311cc3858c66d0622752b1cbe2805948374a07530882f043a4c434866d1b22a147d593afcf2c7bf29f3d49861aa058ad75fa40ba86d62673897913ee9e1f6735a1620481075d932178fd52df627f48b7aba63f0d972e36b7fe02e66967e9476e65a0c9359a30f5328bb617bc20a7d3fda2e6b5d2fbcdc982e7085f036817155546bb0af00fab474369f0a7844342d051d065ef571944cf7db1d64f132606c41370c05f5d5b3ec73318e1120964e5da782156a697b81a73cc1691be1ee43541aef7167008707c6a98a4a9f6e80d926cb59faec17da14c40c82b88ad5c2ad53fd956eaeaa5c85675dc74fd3f6438c24f6ea6dc182c6bc4956fa64591d9eb2d3f83ba4c279f383321ba56027742bdb11f5952aed121bf2b0a43a433d76e313543196dff872761621afb3c49a495be9c0f5b3983ce3da1ccc6b753a9bb44c44820880429ac52c725f8c60019b2352e901ddc2011b252b2c22d82e73db5c034498e36799915e633d2257f406edcd096b96a3a10bb25031ea48e5c1e3db0c911f3d6bffebc4104b31cf5b27826460d0de624f1e69b09507cb19c2999833e591a63cafbfbddb0e2c0cd7facb02d0af97ee510e1ad4072cba67cb3a8f4e354257585f8f4f41b3e241f6299e5638ca6276748c464631ded6340ac63164fd9da789db758fa2318d480e8f0975e51a5f249baec11c6d721e22bd75c8ffb80b3dac6ab079d75ed545a7e9cedcb05e887c24ed68148d499921cd9e36123c70eba1ca1a1f51ab2dbec9fe5913152a0b754cdd86bef6720775454350f2824a1c4d1eeb74c096199c4add9dadac187b5466df883e970dbd3c67d26bd632a30b883835da469664b5068a785055c80901de79151d390caa155ddbfe396982e338b1fda24ab85b8e8a8103943e2cc06dadac964f4e0c087651a0bc0bb78712ef9b2ecff5deaf1a2ea113da2e31838c49361ec4cf6b1329d4fc15054ea515ceb8377a0ed6fa2f1224bc47e71ce77454db675923c9c65c9e6c5322eb320883f2a463e2657ec2a8e4235f79ca09e5f11cd1a764ff575a975f2629e52f30faf0020ce7d32239856313df5a05088af066e229274b00e877be7fe6db55d4c71a7c8b3a31ae89814af7dc277dbe21980a30da333be5c0c7e6060ad03b5f2252c7a15aa55c9e8cb321d53961f4a201b809b688d05f8358821b5d6da40a08c303223d426b29839f3d230306798b75968711f612a10f14709f31775dc7a0034c1712b97269eaa9cc85ac3ea00da07af78cceda63fb2a38c4f626fd9d705a05037175811de8d819e166241b6698100810b3824bd6b857640ac3197df1f754f5cfd7269e2cf8ad6ce1545cc2159e1bd8a27b7e37eb3923ed03c219f3441f5923e717dd86c9e6b5d40617d6eb719cdd4b4ed645821d6a6677fb81571b0fcb3e79461ad52dd06471d003bd6d7e4f19cf18d5b78bb487aa249256f3964df41ee675de48eff8c370ce08bbe3b182004589a50eb6cc2c6476b5544c7e716f00570f84b209992e855d38d95a19a548341c2bdfb871962e1034f04c391546230d325c7173086174552a14d881000cfdc84ae33b1ee5e20a0c992824dfc08a12e986eeb395432d863894f022ebadff18211f55013bce554082247b3c7ea563c6963ffbbfec417364533453e91ce2bb8f95af6f21c0c0ec3df5343b501d5002b1c8cc6c9672447a81e27fb0a261c45b675676e6294529be73d0518ee399130292c076c3de7a3c7a082bd590165c4a5654f828d219c6c638ec38d26ce889fb50f3f7b15e4304c5c490668b6966fde113f68f9d1dd89e41442f2f976184a955d7c3618869eab93c3fd0f8d841b2f1fa71941c12dc96e59b56a38fd75841e9bbeea090935def11b63d9f91aa243f9c6c51dcc8500d80cfa52ae29809dedfa3fea52ef24112ad498b9e584a83feeb084e2f7429dbebf677af80671c9a2991135032ccf26aaae1aef99adc6a36bd5dbc9040ae96f22b7b2e355c31b1daed140469a13f8a4bf648fa9035f64d93e19a8bc92ecf43cd6211c6ff869906231c2fac7c27ca4d1858d6e12f7ac5b321ab12b5d0b7c5afcfe73eba5d23f7544c8fe9b8fffe6c7075e28781ccb8a5bdcd2aaa7e771a181508f3b77d2e350c2cabe507b3cc00ea1b86d4328aa261a827507a2df9c2cb42d52ef890cdf7d72d3c301a48ffd5c54e24e3a3c3db67f1288a5572b186f59ac78a93caa43dc05840ed0fc1af773e80b5eef0995350ad91c21d5c94307cd55488957644a2d3f4379dfb8d9c8a3cfbb3babcd66a14c3090a122fc794e3e6816a8ddc9db8f192be1a445139a85a63d000ec7c9ff6ca5871779f42043997aeb21326982391cdd37e0c1a9992f6eccbb71ebfe567c03237548cc8aeb06927134a972d347bd34cdf38b29d585527256df6d3788807c7838a041609fcc84132213e72ffa1a23e9c69b7a8f75ebf458d421e1c5305091071dc34391a9433f48a5c925802d4c896af3fc8c4e53ee1bf44ade7928ecf17991ec08fbcf1dffa473b8f3c0e3308a67a43d55232b0e7348e71ad16e7f889a0a5f67afe3432c7dbcd49fba9bd26f5b56511448b82d3906df0c1e168e45f2d4bf37175f12d089fcbeb19c8c2bacf664f32d03e77087e386bcb4d462ca66acbdbd7a6f8aee6a163e5b6eaabda3fcc5cda2581c739d245779f96c9f488e3795faaec58a391d77a149a175f3b3bb4bbdb6623281a88df965d04d66076e4e32c795bf92351704d92e9c1049c0573a970e1f156a445212f00aa6b91ddfb48c2b9d2d51b3f79c6c78442fea926e553e2cc4a46e07a3c4a605dffab387306c6b86538c274f66af31cfebde3af50180e676edac23360aa99b3a238e3ffe53ce492ce0178d4a967cc57eea6d65310f0140dfefe0d5578893f1eb69485735558271839e3217c61940baef572c213645f1ded8ee44779c1b14fa665dc87874984bd44aee2a9b71e61972d7f6e35b8b94283c5aeaa71d37e1a267183c7b8dba0aa641f856fb3bf49b9e90fc8d9922651776140ba9efc9232a73e336d13bbe744e56ebb21f82a1fd6c554950c3dd5f5482b7025f1502c791c89fff2cc597adff0a9fdce3ff85482cef3e0a95e257b83966a42c4fb385780ba3a3004f108d109101196304071c2e94690d72f3fc142c7ad29eb691e9bd1c73f584b73ca72e3dcdf28890ca30f91a11ee87cd441c1b0ab24c1aaee9697633e7b41a3974f90f4a00483b4fbaad088342bd57808a730a0b3f2f6ce895605039a5b27d2650daea7fad25481d9f8086c2f5af4f1936090ebcad554e6910e4272e1e00a1c0fb47094d534091b19546ca5a160e677b130aa056f16fc81cdcd27a9aceaa7e33f6654cba46f4bb264f440209047695c22b8b39f862cf3fe21597c375e1f561f84bf9dd3bcc033f740f1e5b8f2d70a1427eb67de73de7cc107038a3a122df60b50b630f8108971bc0804406715f329e04b688a4e82a865a59ad63e9d296b2f840f5e2196850185546da56d84d6561e6279b0c56e95363ee2a525a82339854b3c68d748709c0f8ee526b663d7b7622a5d63fa63285d61c1775d2118325d60f33b894844373b7aefc98d5e171efb9b67d046d03a922ec046c4b0e25a118bb908ae07386cc596fcffd82960f610b863b0e2dab58f5b9e7febe17b7cd1f72ff829bb3c62ff546f3548a1825ab09c061727c99c4b8c6ac0f397a457bb8aa2bca19c5ea1c76770ceccd09024a00f1f3ed32db80644f49fb54680402cb302053c2bd4912ebd6e3634fe3bf4b6b9cc42056848b84cf666e064cda0e44f3cb878c1cf64836007b2e13deae87abcdeea88f57927fdd9c38cb8357f66572a155b41593c2215278ea18ba7743462b857f62c90b382356270486da6aebd6c817dbc6c53a7f3b9d26d32d1e40140e2775aac7236f949842109642a0fbf98de852c36a331403ab4efad459ed4c65f93e62ab0b80d31efb9dbadbb0bf22e2e3feaac853b5936b702067169ed651a4c01ddc913798363bbb492910228d25149810cafea9cc087fbb9caa05e2a4ab79a8a5087309e71a7de562adc48e2b98ba8dcd846f5167744c5a6cb31fc71370d1475fd6e7f3d75709f794265662e4409a2636a3163fb0e82202964b7807a588cf276793afd468c76a0d4a2af493c46b0199949c9740e8cd51451026a95d8e89fcb0d5fa920ebdd23fa30feae73275ff8c2ae1eb314f6724aec1df35dd7e7b879ee78f31f1831c22c1f4d4b2e73e83c056c9835eca8ba705b2a3a2b8eda7ccb87c52756b34dfcd22504fb945f464c799d869a7a21720b115656f1c38d2fa1fa36b4c17462d8fbbe1aca2097ca3d299bccefb4aa48e0a56d8e9980d8f6a2fd178a3c7cb29b7e09477c60cd9b309c783efaa14896e81147b07050a31babfe88d91b7160077f2b97e69a422a7c8b0bc18a22b5529c9bd17aaba524f820dbc9ea0cedf98a1b832676a826196187ae7b4eaaf25ac65d92a2ed9e0a8de72b768dc6d0984c0aaf9f57ea5fc2e06d331581d0b6ab246b69d394293a4220e67172c1d1261e5674a5b9714ccb1217ca537ac744acc449c1f4a3fb29d8d2924da2c6b3ba427e95811887188d106bc1f2de5c88787360746d427675d59247489e1160093e3ce734d725c8bd9a3195bcdb9c74d4e032ef1cda73c5fcf988efc045bfabb400f9cfed50484e1dcdd25fa57edaf072fd4401314d091df58898a9fb12d9d029e65ca64d1198bba275efe9531fe92efafcc7e14d9ce8ce5adabe3b60fd0a68aceed67b7e1da271905009b8bd0dbc23caf8a4954a4dae256f65e0fa3e1d5ff75a797112efa2877b16855cfd9f78cf15b82e1284eea8ee69228e67df878461aa9fbab9aa8692cb8529be157923973eb1e96da4610954029349c81042edebdd6202035c4cae6a90668563b464e4035598f0c0b52928cfe7489ccfc98b2fd8f0ba01a1cc7f0b1f51f2fcdd62466ed46a567be3677a38238e9ad0bec8b6336fda34254f22287c2ce8aed294d342d2faa15d15f4f677114b64ef0feac98e60ad7c0c9343ce52a05043dccfed3f8681a7984a4f86ae85d44dcf5b40d783b6cc11995bd54c4abc40129671ae6f94c5eb475e209a782f9348cc754fccf2550f5483c616a75c2daa17f3d2892536009be7584dcabba2185698e045a260bebf4943640f6540556a4d41689ce068b47f8bf26b901729565075f8fc3bce15098bcd3978164ccc8441487f70feb59d938035362c70e6c34742bfac623c0731a2f9db8292215b0166bef58f90ff681746b72828d6408f2ff5c4411f7f49cbdc24fd9536c6c403466cc15440425ebdd2ffb71dbc37183fe686b9401d9d8550b2b7af9af09573fa9bbbe3e61bfb843a1a3369371769eb05aa3a005f9377b705ebb849dde888edb27015b3cddcbfd2ff48696372d5aee68e5b67011765acafe67270277e7f7ef7f9b9892047dfd32e05e037864bb41e0baa72eeff54df7a1e905be57edaac9ce0bcb356c21a1d33a971c93a1fb994b4468d6719a33e6e3d60548549fa0b7f059f1c9f2879d08ca7753b7fdb2438a2e484200db95e69bf80b7eed20482632cf5b170f26e4fea8875f12595825470c7513ae990b54ba6545817f708f82bfa99a0c0e78d28e240dbc002956f78814bd9e7c78383b58e5c2019148471a9b5e93d767fef07f8cfba60e38ebdec4c05313294a7127b1f0998384d389be06eb67a0398fcf7a188bf8c7073097c7b49961d25e897fd54589d5c4ae86fee8ba2247b83adb232b13ee743af25227ac5e4ff01759e9fe9f16d9b4c58b92723a154e6f7bb9a46dd858d7296afe79852afc46dd56d77b4dc1b232efccd4d23436ee2d9f03fd6fdf69a5f7b29c334acb5f39e4f64b5398a6e7039f1d4339410c52d7e0930cdb54c9281b4f0f427507c3a5536c8c6afca21335262bd6b2e749f7f39a7bb9a7e74459faf1c78eea3d7c4039918e6f87ca316ba96860362fa3f71782fcb1b678dd80412cdd55cb3d868f83d00574eb712a1757a11d463f4a1c017378aca4bf4a1bffda565236f8ef11134193e0d4b6adc7511e0ccf7cb89205278a64ffb6d12100f84204c5315be3334771c531bce9b378ee82d6457248dcfba78e805f00057b220ffb54b3935008bebf150e30edeab6537bb6efe9af3ad416c8cb92bfd090376c81111ea7492d3f60ccbe35876b0a978458ecba407ba05a696019908e3a1a81f9a36ee60c13cd9c2a019cf9a6fe65062dccf7e85a43feb67a997508b3f0e2bc9c51f0220eca7527f5031dd5c6d37595e2534f8b774c36ddbcaba258570a38350a1ff723048b012b7488bb36cd274bbde4a77ff4433cc13d9f88ae67baafb0efbb022b27edaa2e8b9472475f1eff73323c9651d2930eb5b31f06f60815dd749efb04c3226f757b5b5ae9910d7fce818c011e44cbb306f4a9b366b100aa0120d8a5624a2b7781e1e58a572efe0531144977eb87458a89282c7b50c146faa0cb31211dcdf129ea5e470ce7d7b240135036b6b4ed5543ac972476af3030e40c2d2d5b9e80fde151da985c5e4e0f04e323006a37c6297b3ecaadfad1cdcc9266ca8c79db82f1100cdaa02b0edb36ef5ae83a6ff4aac42fb654d28ecccc457a9b792c0f29b22c6552406cae6fe0fac1bfd6b0890ef6da5d826bda9258bc60a706fb84266aa41585920d0b83f7b15e75da8d3ad0b6a85db819c0e6f2360d39b56de822289a098f49255a005a6dd0a9973e2d3cb17ec5a0f575c15e53c3f59a8e04b6108f496f7fd21c6eb8af8780b197a7b1a8f28af6dea5b254502876d846aa5ec8695422346f6b6cdb06bd254611873cac94dad0b8fb8f195405384fd413229a99778c25795e5cc7a60d2ef1cca1d43f228b7032c8d39729dbac9246da980ba7253f6da5b4914350ba614dcfe47a0e4e5b103253ae6012ec864cf17ba907b017da929e9163916889fa14103f78feab128d1e8aeec904bfb057c532ebc11a017445924fb6dc6321692f4c62eec26af3e17d58f28a017449f2cb973eb654dc65544fd75e29a262d1cd29b487b05b3ba44cdc3cd9ddf63c9969dd9eea334482a12665cc65a1b8764874632ead4b9afcbecb348268b2c880c61f459a4228e770035c35f7fbef9fc3ecbec4379b0e4e075e15b8bc9d6877e0d300844e8918ef52538d1e8f47ddea22194332c6df34d810f30e4ef0f3d4a75195827217a262d96b4c5e38c04a6f81f5a7b823f76389b1998f2bc6fa2190438248a01738dacd59d3946d35769a3237f848c0d183b6ba5e44beabe1e89abcc3417ba12551f61693094d784f4ef8b3303d3bcb776c96e41aec0fbdddc3992fe387fcefa11cdb954c657249fa84eb8d665e525bf309b4d6b0f9c4b442105e6efcf3bcd6afe42658997f3f4c6f1efaaf4b4abeffe78fa904b9593410bd60df66f325d5c3caeee63f85c7a16adbb96404346828333e025c7a25f719d91cab0642cedabfe3f3915fb64254015217c3efba495caee0bee6f3fef379b8dd16191436eacc1569fc74a4fd0546faa8f551e2812e373a9d79f759a53ab1b51b8b06322700312eca092714fd51de10b5b97032553ccaf5785215c2af99d6c3ee8bde942bda2083f15697af1ea94fc7dc3d850363b461d5c66d68e8cdb730d887177c2c2ffe8cbe743e8403ceecf67c975a77a6597de7ca17d4d32e5a4eeb5e0012c97295d3773ad5b6eec1c08d377e81c8441ed7e15e9165c08d9defa7ad1ee90211939ed622d3c87c07dd36b40c732ade94f055fa8a3f0a39ca5b67fb00639e54aa9f3ce371e8dc330b2740c0286c8fa83f42cc1a90effc84e9ef57842d084d4e8488651f73dcd0fd151b3aa410ffbc424cae75ff25cf48ab53efc00ac05ffddba2d039396528bc10cb3d0fa95c34853fde2083d2ff4c6dfd6b2e23ba33bc876feb5b857ad422fdedc25acf50b3ad94892abce274067a721fb774a4390aaaf320f08e3204e8bcf7ad52599c5cf2cfd70fbe9566d257ee6370a95959092fe0d84c9ce7ffb5676cbec8936728566004d4d62cd171c4ccc4532eca366a41df3ac20395190a9bcebbb88c686deadd00e901ac6fdf7e63226560d95059120f927c68bafad10c33d141e0427fe34260adfc9fb91732e0d92919330b505b899a385d03f6eb3c27376e6d0c03b99f059fd7f2ebe7cce157651daf76d78136e356d3e44b31e7060ad298dfa9e5cb41171281f2fcba485bc5b4a56c291c8710db0a8da44c94365238ebf6300d28e00fbad7628321afac2b23922915cd1670713a1c1adece261a8f30f95bd35858ae54dd7a4686962e382c15fe1c9c004b219a6a61572bccf344548e4e9cacd32b630da78b2539b3abb6e725c2d4dcd7a3815345e86f2a08023b5bad9376a38c05d57df7c7860677ba1cbdf66f4aae96085a6f9af83333ee4c4bbd246dc1ba77e97b40ed3d0ffc517486e2e412702012f1df41aced954aed9ad7ef5f6625e72d1bcd0d500810883090610a347741bc21460305310fbabf856fc95489e0cb5e54bd1846096c565771effd1184d783995740694bb1048d6bc6268411fd76b5cfd632bc209df3665b9c59afec108f6e41d6e452772bcfda416a35b70e92e9fcd20df01d1bf89cb586b803d4dfbc680cdcb0c7efa394ca38fa48474f14f1b8ab81b468d2d6f07b092d25a71f44ebb355f273f3c994f9250160472e3b4f234f2049398e988ba1b000dc152664ca78ec02fc04b226f851318b32c3755344887d5946de702ef1c3134e066c85815025eca535e7fd7b2319e81f09aed05f6941f3bd33992cf014eb4e16e542380f14035274470c08f13ba137744a3a40f7152c26903ae104aad0a3869aaaad700efcb6db7e463607c1c1bee714781aaf36a0d7fde0395f0c5d09928ea37a9b5d346822758666749cad0e091ea12240e949b019cffd39feccbdf3b0d0f0a0ab08154f168ddcc31fe11cbb425a533844acb22c98e03702c80d7c8116b88e5aa7358d692b3d001330ecae48203ba97da35e9599e98487b84d66d132f119b9efe6bf3904f00fb9e9fbc3e1fff5d8bb317d05284172621cf87f744b28a5741cf116e6f3e9fbcbc13930db4734cba291926e702e9b121f4f51a22b63f402ff20056ac65094e79f008a62e6a47afacaee84c27d6a834558424c8d8e36dba0b6fca946622f7e75d1038f0dd9b4fde4dbcc72470f4250be91b81c9ab9911412ef75dfb98d9b21b076d6908958e9bd554c00b827ab1b82485c7de949db642dada4c2a7034f3d250d1995bd12e8086ae3a10642a472681e7e4f7bc8f8695046e5cc7c24eb59b009969dbcc0411c2b002ed5b64201fb3f0431360659d6a04e79f60c2553aa182ec4b9e9828b00cf9fba7d40237a0ee89a5dd76537c8059309a8269b430e00f34fe372f1f566bbf71693aff0d79690c9de25a1b216dd80a346b5f64bbccea9533a224df2d27f82e9230f77c92c6f0b439d154a9efdd0778dc5d8b0e4132fdb6af9365a0250bdff903537e9ba368958c9adfcd97e586c5397b164e09f939e5e6f9fcf5d6aea189c0b4810278bfb2bafd5a0d7aa1120fd911cb713092b651df0872c88b931682fb28789b964bb60c4f750b317c1f898eff1c7db89adc774b63a8993a73d5dc4c76fe2495573779bd7381e53914dea12df65df916fae735bb31e159522df448ee8cf81f0b2860d4833af38f9ffed8545328cb6593101136b67ea34240c16fd589bd5d64b06c23f883258f16c0eea1fcd861bd4d678352c11d386438a6452099949c372cf9c273d1ca2004c784233dd4e88039138290fca78f632b5bceec74ec4f96dbcb6e5f05234cd40237e9ecd9259b006253eb4b1047421081be46509c5b9f313a8a69d07e6527aaefd04cc17bbc0e0550a7a8820ba8c7d63f2ef8a479970d2f71d908bdae6996590197050185b268817f15c3018aaa28256f05e9b410363106b6085470a9ad6852a6b36d9f00673d381438bc8acf1ad810d7a20e4a0bfc4e81fa8e698c1e16c04234db10abc5efdfdbf5e67e36f35b357781a8dfd55a0752ab1ef7c69e097ee831d690c46c2c86bc91f3b6fe7ba8b1004dd334f9d9b7de568432a3f40fe4aabd41400a4e6152064f7ef52ccde0fb2e9dbae5e2edb2bae78b5b07b2696f10e5e0fa7bed5a79f84194dd9b0256a908fd1a7cccc12cef3d28f7be91bf1612556890e91c5c11b9ae559817fa0f27f35a97f5986319af112d142c64c7f06b3f542424e7e129947cde7ae7dbfb63d0808966feb8ec17c6099b6eb6b75e17072419335c3bd9f8d6e5cfad3db78cbac2fa55c6f2f9f6fa98dd0e8d9593175f51cd35faa555b38000335572729c8bf3797515f78bbac559ac5eff5d30f62a0fb47613351dfb3113b6a99f5ea2a88403a85799e5711c26337d31f122e33a78ef10f411b4e8447e13b3597eba65947bd92c192b8b3f276d8ad3ee9e5d1c70a358e773de5c2d440478ae9d3f5afd39229f485bbbaaf7cda19bddbd88ac6a4134b09c0eb0de834650e12c7e8d0c2a81d9b1b7b97b730b5cb7d51d2b88348102448a93097e3b1a31872d4148bddd846a3216ed26da79cac255103a26b32456f0d60959227f11122ca0784921b3be6be826f2cf54c0bb5136764ad9b90817bc3066d585752813442ec3483d50c59ccaeb06fc3bca0d8aac3a1ad3673e647d40947897423b3c28ee7e932d668d0bc81ea7cf411ae364e5940b34022ca686f1fea7461d2c3d2389cec422b760512fc15b6385e34216f1847aa68794506ec96b31d3705f5d1010e42a2f7a03254506630f47a1c82fd207be1a4c5a462200f112c9dfa42c594ec012d7bc1955521ec179beffce188345375984e8171806c6f1dd783362e38c23c8d566a5df4a7d0685120d8e2b515e12291eded938db6af25f3cfc9e60196244174a1c94bc713f027e75dacd106c1a19c60917d14b35fa63edebc11813a156061f9fa813a32f9afdcad92651a13fa73fac0404548b16cb65c69d926fcee681f82cd8eb1b280ccbb4e3bda92a63802185920d795ccde16db972489443d39131f64da13158fef28f0c532687887d31c39f1fecac96000e0956272f1d047ebc9133f20b7ebc6ef2edd971fc5b502f35df60079f5b1731fb91eba42cc49af0c5e9a08caf1ee14b51e37665f30101347caa7183bca92a3abdfe4abfa56b3bf79a815a8f59ea3bbce381e6ae0ae65b552aaa901dff0bff5548da03437f4749dd2d4f42b1e3c0443cef71200feac65e6a0ac034b35250786e5f3015b7c257bb895b3783dc193b3b1b37bb4741e76f2c0502502708632788d8bb39a6a85e3583d88b0e6c4c1bfefc312d4a9efc09b09d57d66f4a499b0416bc4e92b671a88d00771117ded4fc266f561e3677a5cdbb1ff16b728b062491ab09c42d19a8065b53c883f74c3d8851c5a47a158d98f8b78ddca083575876912666e9525f8fdc80e37556e61fce391c9bccc6076a2f45550c88654adbfee1590486a3f438bac22ef90fc9c2574c4fbbfa2068a3bc0fa6c9ca405f5b2f71cc948b6f0fc57c70201115b9d6744465459c068845098ef76cf331f724973783fde4b91d4996a6c73de5c86cf37ffcd207a013c2e67e0df19543589971ded632a56c1fbdcbcbf78258bf9867f857703ea221ac5f81bc7d1dd8519a09bfcbf850bd1fa507d2031e4e9b94c0f8afce46434efca19fc2e1520f573d2ea0084a3115c9cca567aa34092f3acf96b577066be5c0cd25b78c4777bb89ff9c528ded5c30711c12ef0e5889b733583a4c5b48ef23d5c9bc5ab7fa1cdb585084af0ac4e2b7c7fb6d01bf4b565a41ab846dc1ebc5be6a7151967a1ad1a31765768795ce80eb6e144e52697edf7920290d5b1d4798d1f374adf4632f561d1561bfaa742a84116d5f616aea79266677329dc9ed1d8da1695896e121e327fa0a334f45d9cb77300ac0710e4947f0d8433d8fa6603a225b1cec3104fe0cd4bbc73a3c9d3db705055b97e34965903aa72b1429ec9e523b20b0588881e5ec24d52f86658420918a1db657629b8875e38d7e680e01d18d1cfa53fb1826022a024617bbd7c9bcfae7a62140f3639585f0edc8acb6383743b8fecc020a55d702e7d86a77a16d95131dac0a91091cbb50114dce49c3189476f7fffbd7822ebc16dc213cc95aa6487e2320efc161f22a6e32824db0c88d8b8659fac6cf09157909e7b86d9dc076a796e44534ab86e777840f2ff0538a8a6ca414ac5286b7f445f0b786dd722651e5dcda475ca18d8dbbea6c8205f2fbe7d151a74d46e78f24b8c194329664cf705f87c446fa620e0764eaf990130a9ea6b01f7e3bd965545ec736c292110e943b219978f6ae5cb2ffa5a38a41d71f5e449d2306dbdfad88a911dd8b99127e904b3c7adf256933de3252d0febff3ebd29f7fd9347bdcbb61d4ab5e207d9086909c98d29cdb5392023d17bc349681c4a5fb14235ad787dc779dd1339bac7a29c7f9c69919dc8fd586ce44e62c646734e22ed7ce80874e644a195b86791a91445b536f5e08c53290a2a230cb323c9a0561ee94c608bfe0869f7190be04c45378f872d3f449ed05557c77573844eae8bf8a9e00c98abc2f765ce61aeafd44cce140b0ebdcf3e257261fc8a0ffc33efa7ec27752103bd4947b493f09a32e781f786c4cbeb87174c67da15eabc1057083910d3396827bd3fb32d8091309d59e28bcf74c0eed155ced10e5ab4ccf76ab6fac0f4610037dab6dd7aed6e40ae9a6f6a0bfa7262037a09272a1df54981112c12a56c50ecf8dfe038ff070d5212694a7749e90dd30b24e04400129a2ffc6f2e078218f180c3fa6524651f02cb42c0fe63d0fb7e16ef363f6fa40a10d8f25979647056b4c3b1ddacacb9b214343e16f64c21f06e4440afae85c7887e7bf102d66f46feed57d973b90b252ace7f2a2e13e79becebcdffd8b0044a2f3769cf27b88d855d91c4af69d2719e0c6be4718a3ab74e2d8dd743dc24cec85d6926bc4296704a79e3b003c126ce5a540dc3c4f1347517aa6b0370c169bc2f59ec7ce8a9dbaa8b0e56e130261fce44bf0dcedb186cb3f734835101a06fcbf7da8169e5f669c3f02e904a1d0acec151b277fe92add12b33fa4d68c41c7abbdf0b1a04d4dc4a3f7ff9264bcb89fdbaefacb681397aa38f954fea97ba16944020238a22ef9cc750c785c14e8e2f9bee95521d9236fafef6eab8cca405a33df42ff6f566e534fc6553626e6cc2e7bdfafee8872b4a37bd6ddae521cef38e395b7b41a7e4daa08a190c4f999255b5431a462a07d72859b7b96227e80b218a6d53140170cb4432b761b49fa6e97264d8c6dc2534d57a2e19fc76dd0f50ef7bc7c1dc412db9a90de97ac2ec5f5367732ffd020eefccd4933099ccf9e8070c21ef3e4d0cf9b89433af95255e6296955f53caa729a84072fdabefa43ccb8860f4fab3522f394993da03ae657693292684e2b1649da726ee93cf00403af92ad1fba7ecc4910f81f701ef34b129f6cce2649f2889713b21efea618d33b69744262b2f8b0c80317fa673112f8d10dc47f897ba19cca65dfe799f9880ac2b2d3f47afe7b03d3e915fbde10adf1f42739cccec11d6f047b3ee29d3d7453f3cad69e5c748a6acea3fb3b1d9fc564523692755ab1a8ee68502db717f0199caf9eb8b5da73a07b49934638fe4018794f0d7679641aa9e006cf0d09ebc6319864307e60dcd20b2b71c4e7de9e2bb3e6820c8058a5c1ba9d4468601ba225b841ec4e8a2e95b45c2861d168fb7b0582fff65083bf7058af88649514feb073e613d4456844aabfb7efb822b43e34749e21ce42252ed692eb4363bbab326b4d249c509fd5fe6ae0ac307bda37b6d58f7e322a17f11b22367ab60dff8e96ab953e3508b66b802b9f7971d267a02631504ecfad87282d7f5723601155bcad4f64167fc026d02de6705e10b43e9c02b1dc8c8105026fcb267102fd4a41d25ee7f9a4e7b41482697117ce49dfc97b5119550ddcd17ad5d0c6f72efdcd2180d70dd46b0bdf1ac5050e024504728de4134fa50d1f67f56e2023d14495d9b8a6ee076b880c5780b7fbf0d1e7290dba8265e1b1da5629e4dcef0255d3bfc3c7647b603189d6a3802f0dc9a3824e859628ca013286b875786289eeea428f487afb1d3998197aad5b63c76be71d702fa5f27b84f997977e1f901df70c3f74bb1da6f45a343c352b5204eec200f294738beb11b06e6b96a3df21cd4ecdcb4aa73b073780f91fe1dd625b01ae035ce37c20324156d60e178941879aa65330c3e74b909ba7d762674e5d3308ed50d3d793833054ee4faf74293c659c5ad9f1ee20fe643ff1774619a4c942e7e41cfe2c7a525784c318f1ee8a699e79bfbe2f220c0870944b013c61b478ff7348a35bf89a84801b5bb3df96c78b9c47b227fd45e8fe174864622c2c9370aeac65122a21890ee432be9fdb8de26d8d0577a07480030365acc4f1485108755249832348aeddf9a9fff2777e50a958e3a6c4bb9f587ac6b6885fb301f9a9e0c128caa959bb75c465dac5f5b3e4c5c198085035269044152b1251474c5e77dd35d83e808b691d6a40dde3395b540b0c73130633104cbc99ba4c8538b53681177eddd8d7df20b12b414e55218e1c9709099ed75820f433ed476386bcbf53da99082156962c107825f7010d7eb5c9f5158ac736ef289827c95f39bb4e46fe10b4e0d6ac5191803a0604006b17454130a4cb27c610900cbc01d9efb7268d21bb620bb97f1d324d4c275908ee13f9b16527ec53ea5feacc9f35c82aba5bcbfc9c119c7a6af9d6d421582cd0eea9af583ac14e13f0be641ed7bbe01c154c04ca7f813a7d5ac784ee5b72d1bba2e3a9051923200a90ed485f3b8fbc33f8ce2ea8e09b501892d00681e13b10962043ee8c7cdaf56ba6c43330ef7387e3dc447e27dfcf7e78bceb4d2a461007441f193702d6320eb2b10c58f4459b0dfaca83c8d60684e6da9a0bb53f51b239a92c0f90fb505ec3e5cbbab74fbb65e76997631147d3a9a981d65526f03c66ed5014cb8d42a5bd7226dc9c210f1ae04c594b2277adaf76330b27372b3cd8b226c97ec7a9c717ef814514f324d4a12f32a50c1d00fa80c95d5734d6d9a4f85be94dfa40dcdc8c72e6e12b9f4bccfc0fe5caced887f11a217fd52ece211d764d141e47fe1ed0bffd968663eadf07fe069a6ab508a74c38ca2b9e6fcdfa9f9803c649d1808444393206006f193c16807f85b763f11aca819b305ef13386e152e1da1b30f728e83107b9ed6867003ae2afa4fa42e37b57d5af9d94bfe76c5c2e018c2f6ab57cc4edbf9fbe303a1c28ed2578172ef1d2129b6a74dabcf400a759edce2c70201b40d2251d716071ed724466be26a00b3e48f4108fc57599bad175caba150a9cd6e7532e8eeffb0882f58999fc6ad4423e748bf6397cda10bdff6d9943bbf1c2c2879b44478beb03d2893128249f555aa2d22e2feff85d10b3d6ae0c57222b075fa5938e33f3218e7b779d09ee6d4094399161a742a7f6c13e354bdc8d7c0d8e7a3d25efd0b8b547e0aa4613af0ccd21bfcb087aa4d1cb0ed885f2bec28e979f2f2284381d490e3ef8f37644b4272d4d7efcce0144e57c1737f83dc89407ae0fb72bc01bc4566a4eb81f18eb35dcaa6bcd2733d4c9e4dece4cb04e5b1636c8167d57fb4d22907f797f277fd459fd37baab4e637b4938e6e99b980ab9bd701322fbfd414f5effa0ad85f293d23885d8b1bb14c83d5e1dbc49062201a9a08725014a094530bf34c04b10f8ce865288f0ae8507f4c5d0518b8ce2fbf836efa29d3cfedb362762c89ceb4fd017d2076a0608964d0968bb2171f0093aa90c8961579a736706d3f65a33847e77dd39efe7f616dbc51599686ec17f12dc26fb7acf6a98f34e26f2e027c19bab9b46cbfe1c8b54573abf26d86c4727d1d30c2e59b26d81dc838a5896a672e6d39200a931f9e9c1e51ca4a61dc18e81c848806ce71189b325858e258001cb3364b0ab5c11f72f443fdf273c54355c311168403fa916edb422859bcb71059f4da4795ce5395972fc0a29228c84345f6c4a0548f528e0ea67881d9f0347ef86e88a515d5b3bc072c69986429dee850df7e90a8d01cfde57aa28f7402773491c2a899fe4f46afad0ce3fa329d9ca367e3ab61e99798b102ec70ba2cb7e4eb93a799e8de8bf4b292e1eefd2728914b780e6775f6f011b22b716dca91d3ac05b9f6adaa15909d030054dcce6bf0678e64d865be6a4485fe757e0c559151f869b26cdb4366ca9239a8a8ba5074baf4401828798d2f5ffd02e4d191fe14a8336e8326c8e8691ebf478ee3885f2a07cb140aa9391e60fd62e744cf9a459c7bebe5034356cd23e8e14773b8ee434ac20a91f7933a358510324515cfc62bb8b8539b526cc9ddf9c643dee8a1527ea257db42dbc9866da1aeb47557502d791c8b8cc03b342e5d474fe875b6c8fd043f6bd8552b8355aad55d1cf5aa6822135d32164c6ed9ade4de7d24666e93b45fc17da294f40e25614f7170ab0966bd6a933d94d6ff35164676594c155a8841b9833c8fc81551f6417aec225a4db2b358a0e35547d92247a6bf401ffb29ea98acf32676612e7d3cff00f0e62e933168dff99167ceb9841e1ed007595649ee9f80c145db28d2a167b22c70127b966d8c1472a36693831624943099972873bb0ee68e7690985a8555668a65273c5c9ed12bd081d786cd21868b8eced27dff17ca0e57193e4d7d7ae20c8eb7825e1ef5a325f3db91f7a4f7772f0e6df2b6b3cfe7d9313b8cdc9eb67d19e2e6f3772e8d08ddc63f2f5baf6437ca1690203191261761f4a69af3f59a5d7386939cff063a7272fd696d6d15c2fd40fec1b7d80c971b23224f7a7ab09bab9ff80ee19b517efc42326b82a068b14090063b4f089027c2d1c62751fede1fba08381b3da937302d397d38236a6e74b874d63c7d391f72939a447adc5c150f0770b26c556451662daec66ed01f8cedd2b30d925a23bd519e77b54ea9507e8ee1c220ec1c01756bc09dd567ff2d2317420d78cccfa7460b7efdab9d6fb9123e5cbb0b15be851242f790ba11e666ebd3f6f7942157a12ce75521a8c281aaed6d609d2e564b02aab0bafb84eaea5a384549e0e275a203b190462a0216b7671f033983caf010647c87e5c23fa8e2298f32ef0b9f8c7ca6d4b41054172749d98fbc22107dae487535773536d3dbaea61605689821cc65e1c72d5cdddd2bd2736a8808de16b09c29804ba5b2a4cbf4ac5ba395967022c445fbbe1780814de33fb01dd5de5ab70fcf956b190a2da80489185231213be78060054dfed250ec03d0f6069e9f97d52c0c937a3014484fa9a04af0e645a81e61ed80b5c641a883c81d52781d2e55c590e9bab0c134104c9b44e681821b985abd66dc50a2286c2c640460f099916feccefc3e71c753a3c82adfed1102dfe3c0a03c733d8d1effc8ed33c22c04e832d70c1e6e585ef2aca848047482c3df6d750673112b2b64f7e7e72a6c4dbb7fd0b726e2cb4305a70b4743816a7f65b3dd23100e426793e05b05b6f371a3b9fb085234147717bb847fcba89bb299de2f7f4df227d3b0d4ee590086a03fd85b5b0e1b9e814ece7841027f5a942334fd7dc49248056094fb4c6b31b2dcca47453c7ea53c36d74d48e48f2009629a9d1f3166596bb04dcea85ca8e8ed9abfb76b86b80857431dac6f010284db9b83f26f65acdc7019f826a2ba5ce8ea2f1e123c81f66270e06deb756166925edace602c02dd55d56781263cf377962a62caf350d56b030fcf3468174449866842ef3f7448cd678cc1ee93eb680ee3c8b3136bd4406bd77023c1fcc81a1e2ec3167cad8aa25f599c6193227ad5d56fc8145c5c1a26c5d57348e956f520d2d2378fc645b6689cffbaac0a0b466cbbfa8fe01dd5c2906c2c2aba50bffabc68519f6fdbbb67fc059b5eb7563db932a003202513c89582213d591e5ee4b362add935d7b446935daa09cb2f67bb1ae852595b88f43d98aaa9e899ef1a232e02821a323a4a6e54b466a6803cae9ca05d0bfaab0eb903d9c614f0c46a6b0e8cd72f210d2a71612f406f867a1b6ad916a1e520d4e08a48c8a2f5e8ff37705556a4c016e51059f361feab8094f40ee0c7fe4aed8c101638ecdc85989f6c4cba9b6a2ad1f3b8cc3812b0682725b8322c4dbae4e8c6a904eb2e1e7e6fa99309f50d0080e660d41e982efef853b60bdc8d4e400dca92136cede718de847cf0dcdd4787a0056e8b53cccfdc93d7aa42ad4d11373947df95a2ce8a7be1772279c650a92c20f0d51f06b5f0cd94e839bee5dd7ccb829838a2c2c2394b4110704db3ee4c395cec4351c1c2336a0561a1c1348b7b9fd13e177c07db133421d2a0546157f954c452cb8d8a122bbdb03c040e6bd047f5cff70fcb9549257c77ff0e762948b01d42ce5db505090e176dd6e8c21ca6a67281e01c8c0463366fae2c5e7e4eaf531c117e56c8b1df7dda657bfcc9f17fa584ddc3010d1c3ba575663fc1163417a9fe3bb271bdb01f119ee2bb64d80480dc81a81e45e90380a41c65ecfd2f9f9e1941b10decfc23036768c25431b887e206320c4d01c97eebb14a1413bd389fc91536c2353d39c2420ff5658ff47a58cc1efa3a2f5b47815ec94241954e2b438215f064aa52a3aa861a86c0df98703b152656b20a8a6027c601fbe08692fcaf9b9ef9b5a836cde61d42e0a763f108bc27bddb5a81f551166accedfb5256523506f98ddf257273642d77a10b42bb8a5c8dd49f51440602cbf73e7f2a57000e1bd279802e2c7e6c106343f6e4848b2c602430b584462c95d7a916c3a3f37ed7c253b5077b183641e8f47d569379c43b4d03e7a7b957a07a8a0fe837a67bf273f08692dcb484b00f28a8ad20a320133d589c993893bd2cc6986ba6ef7b7f875500ffbad6314592a4847dd3f418ac19a4ab500e17d72b9b3720e86e197d355912b4267b18da004c7eb1f2edf0e9691bb6cb4feaa1a5d5fdc244f3c5669113018fce7012c178ff0f6bf576aa70999b88b0a280a539da2e6c48cb54075d295b64ca14869fd7ba6e6994ffd6f99a2fcd8518d02a1d16969e1454a168336b30e56c353c3cffd424851e97efca8c8071d2151a8c3d34498769c344c9740736aa29c04698cc58d4c80238c74e15cd1fe61c5b68322ab712fc1791deb222116ad0341b21f798533dd7ab4206bcef777ee3ca88d95e3da2cf264f5bce39e38a5dae93d5af15301006f06b28090a6f2f95cc4e87cb79a0ccdfad9bf03a93c155912774f46e3840e9ac5492f7cf266946ad6925685161ce382a947e6d8b7d66045ffb5809bc39df50cf97ba3a338c441e9387d7630d75aa0a4c653eac8bd3441697f8160c8255e9653a48c670cfd42d04d606736ae6eb67eb25337d9a13b94f3141fba9c55b3bf4aba267b716dea8c4e5a4228207b28d2fc07a9a473a5bd8dea627bd069b8b0628f6dbe8578196a98d9334d9411aa91f19ceed0d7e6250307c24d70692cf8eb4e634071255326a81f10cfea39a450f2feb5fb82da78ea9c529c43bcaff45942f7513151d525f4ed1e47f080c753f644aef85b0e2d575af55b17ae48305f4d76c56f7e97e586a4921f3c3bd7d46dc541f7aba9326d4c7f291126549351ec7c52f5b51b1ca99a389d928513de54d84d551107d220d77b0a046c5160d830b39aa4e9c66e758a5bcfb76bc9c874cedc2f470f3239c8657c870628ee1243cd6d150202e7559da4abb40924627f944fe626e06a48e871e297c33dd3b520e12aabece9e346de74107b63394bdb2821a321ab7f465e969b1134d57722f9cc6b6622cf53315ab0d182ab2e9c095db5c9a57bcf28f89f3211f5449f035a29aa4a58e3101db5bdbc88571ccdce10f8e011ee3c616219279c9c4903abe6cb6e040dcfb3b907b66b6e409075a0e4c999b62f1db5b22085dd2b53e1bce1786172f2036cff7ab197835c90f52bd0288c66b58345bb71d21d5252430f675c2548dc8a16f7babf146e08b5b75753aefd4b8c9f60f0859cb5afde59438c57fb71de779536c050a7123c7fc74e48fd114d17900795e5bd36e2e5645c7240ab1de27b70d5f6c043fad2b3ebce03700e65c940909d818c175493fbcf11b2eefa780e3455b9733891243b296d7144b9ddf8755e56b4bed79bb52469802b2f212106cf60829499be11adf43faf451da686ea81a1f8be94e46ca9c4c7e098e39274755c5256be6cc23a4e1cadd10c8ef92d4aca970846664aa07d0e557e78a9426cf52f3c618614b38b5c9b03b613209a59d472f1aa4613b1ab07d6b49c0c7e84b1de273444957497f3be48886f08e9f2dba65025e9da6fdcd26de9d6e9d8b0461c50ab2cf6c2ec83a58376e17fc13e57d8b9cb455e9249b49d2751f7071d0cc3a3a26c9b06f3d0eeaf7fce1b7729b9d649b1647d63f8ab4a6cbc535d9496ed36817273e05d3a244344534af981fb6b6d9925edfa3942bb2b48a26491a70a8b863325d5841366b7b0a51cb5d4e781faa646847df6da1ccce7af6bd8a62cece637ed26da2e2badc34362d94f631beede8435568fd0bdca764943a10fcc712edda98c45cb2c1ddee5a2376bd7a433e53bb0c9750068764484dd8c235254d7a6b168229ed2fb9e48af68cfcf615c5969375c29ace8219dd8ad149699be463fcb45fe246cdcae448cbd7f977be243ba3ef379a616fafb37ffc0adf9ad5484763fe083ab95310b1ec14ef79e386ee4d33d870dc5764d69e903ce7cb1442161f9e54880d664ee25157e19198b98889ba2734be603ed38c29b057d8fee2e57ae8992b59883ce737a7cedafb1d1a3bb59c84b7735bf3ebacf0fa144adea1af8787ac77c34174374b009b334a0922389ac25ca443abe92a49a1ad35f832cbe682e9b58c2c2dc610656839eadd6318de059611d089c587470d15efddb9dfdbbb8afaa7786312d1a70b383222ca5cf59212039eaf4a25a16ea4be3b29cd8a10146e6e3675ef80ea4031a4df51d976792d1b99820952c1d174ae0439213192255f296a45b140c35c927d9d17db67dfb9ba09dece29c7cba01d62855a55e2343aad515b1763e9d13db3a9dfdf7624b3bb6b8d8c4a566d80012046c3ec0bb9e3e6aad2e49ded5201255059f910b5463de91eb3d8832e431387364985a2439f8cc87388fcadd27424496a81441dba14c2032aa6f0ae0cbb4dd9c0cf20b1124f8b15d992a2a30264c5081925769ce00df6fa67d1913a7588f867bda064163104b3c600d79454c5121a6588784be41487e779e9265614cadf8b92a9d223da4b7b5759dc0fd7dc0e5c1a774f748b5bd54704cd6e580f965aa71dfda3c715d8727c98ab768a5a3d186246d5f8232aa71badcba83e57a672032bfe6730428967fcb5f0ca43b43b35251e0dfc619b406bb79fdaaa36327e0d685ebadabba03d551bbefabfd96e70766eb17b3542e6fce82a5334cc1ac013cfb34843d61acff768b418faab6c21e22d5449e95b0a82ada1481be2eaabf73e741fe2e94a4b8be2cfedcc4b0796f0db855a2297e7d0b5d5467143d7ade4129a19df10c9729c5ecd0dc30f04909a3a038f206ae656f5d7e6949f33eb66bbf67168dfdc0cc6cf40bb920a5368dea3e84db1663b30f9f320c4602a33935049dc6bbea342e6a5ccc14471c273140e85541620d9349f46a9b58616d18f0d720fb92759be3bd0282cd83f8db6b56c56ad5773d2a7bf61c55ead25d7e2c933123e2e08f3fa76ec1708bfbbaa5b475afdd62287e439fe7c2672bb98cdf843734dd5a4f41d224a24e9058f7d818928b76bb9ea33c0e0cac18714fc7fb366c3823d152cf293acb093cf6c3a3cbb3b20f8ed716baad4cfa3eeb2d30ce4254d62c855cb1fa21fc95d7cc5cd2a43e25ea28f8395a59aa8f9f66c8e0462c54ac83c52e251fa6beccbc1f07f6e89c1bf802cace9ef4c05a892e9d00d72b46525f70b823fcab252505817522a3c2aad4d765a16e61d7b905f5b9ec5a54f82bd0ec5fa06c34f8e3a7d7dffb5d90702c5775f0b1b4c516b7d04e323e625d2f231cee88ebd5106db56fef963239255ce2364e1996d4ba143a4ae310145d87d34550f8740d0c5c6f6a4836876a234eb75219fc53d05aa61b51e110a398813ccb0e63830db1555f121678173c02fa1556239f7606094bc60589abb769dea47fe50752677ced4341aa27d7d18e109704864d336344c36982e21f336edef126b54ddfd327a4c620362d81a465d2b8d97f8c115af1d6231c2017f395b62d58fc8ee6aff58ae2f679ac3fe88a699d8d2bd7dcbda3d323a56bf41241234affaa27060e9ee0f0921c1355139f1620afe8db91160640c305c9726bac59720fa4798df0f87acc651d5190c12a6d14633db572c500904fa9afd343f894eacd40e25d7e38721e83397ad8cb194beadeadc1ae9766a432b6a8479fbf33b919ab7319e0eb0688d1f050b4b95d7f09bd28717ebce0b22f5bd0b515db1af051f7fccc896a6fe178cbd1b249084f6085984acd9424dc682d1bb39689e68f61befe220a6b683c172e7c5b472044443e98e62ad0b35bcb3673f26fa31e6ebd299e6dd32b3b5ef7d8b54255b8e8fbe532b928d0474298b6460c353cd624aa1b2a7834e4d8287120afe4afffc1ff65e928291f36a42d16ea43fd81e191b1c2822ef2a3cd64ea45d7f1116ad3888eabd8e4f566b73b6cae73999f796cbd68762f975cbcc3d64c67c7c7f7f3dd74ae5dfab592831387378b5d1285802a786b53d8a9cb2880073578b735798096eb5916fee87eb4edd5a45984ba9ac75e1082dc5933dc6efdcbd90d2fd691af10fdb7d62608e5effa9f096e99c9a0dbc9b317597ae3b0b02974c6501c17a3a0d6c0271ea5583c3f32171b3a9ad6eb9a1f9d3e765f1ae08e166e1861a0ff099c660bdd5b0970cf0083d858b3421269b889e97c5a8373cdeb99114b6c35f52d400c065c0076c81ff903fe182c18333f7b09423237f8a0d72865f46703d641fe0c46869ea13bbd63d67246b6f3ade7b827739030da1933ed250fc077cde8ee408f5e0a2c1bf0e6c491f9fdac8622082de84d5114605b6810e3b865c9ceca27f1899072f2af8b9275fe60503218a1ff22e1599e0433726d22962693dba802171f940a282d13e2e26130fcc666d396a46534d7d217e417c1b4bd152ec99df9eedf6bae5a4b1afb9ee8bc5914f254659e018dfcd9513b080ea67c22809ef4f7f7d04f72d83a8c13822d204e81cf5c599f0888aff9f5afe5032fd9209b62d5ef8984961d79f518896b4229eb8016dfd0cdf8fc9cb4009f44e09e5261a17638d7e617b49fe013d160d7610128d6b3f2080bb68acfda0b855498115dfc2ffef18584b21389243d6c41063942ec6dd2048e5f95d4715d15549c1a6aede87dddc48e7ba4c99fc49325a070a4677965a40dd0fb3c6b1a9e549dda5103262fe851f4d9504941266bea1a1048cbefc829e6a2b0af6c18220afa950ae29710ebdef0a02150e71515cfe554f8b07ea03e0f1a7a11fd267ccb69377070dcbbdf533727a93f41825717a84af3101fdfd8def20b2809a0a1d40828fd0bd99f54c38abed100e8c4a2246d474a3d4caea98d1ea2eb104d8abf5545c18f46d6eae686228bb4a82923a203df590d751a707f2fe7b80b5cb6a1577fac7f43ca9636814b6e762900f450e683543af84e517376762fc5504973614ad5a50eb4c5e4618f29bf218c5f4d44c9279f3be6193a0a7109ed591dc1972d8a8193f4d0f7e9195e357e3b178de21418396e63c3f30f20761b5989d03595ae9c79ed950bd917a7f9999e3e71dea4de6021dfd2f579293cc9261e43cdc40b6a392a7a14fb81ebeb3af5aa807899b0899c4a2aaf38cc5ad837242801522b71112db6680c7804c9e3ef62c6306b0e51cff4f723b47735c9f9c754c50314ea81da7b47199e0179d5f49169786fb717d8cbbf09e99ea323d91540386d56a38d9963d8da4d8a7b5d55e2310c7beaebd6077da5743b698d285f8718233d23971c3759e009c59526edb99b25e130b524db5fa8022b0fc1c3b64b71e779e004c2ad1d1f07434343c92df4576206fb48d2c1d71e765209034a1ab53112b42b27ab6032b5f80d226a64bf278e58083b8b938a9aaecbbc1a5de33b6b1f62d14b26d12f4789a759b7cada34e9b01f33eb8feec917261076edaa8287f55638b4a373d6fe4f72db67c5ed5e18c431d8d9bb1ed0213de08542102b2d395be5ca30bfdf8ffef420b2bcac13aac0d080731f429f8c3ea03d4c064b97c88176d74762656ac1a4dab7774ff902e259a7ddb813d9de94ca162e680e06f9ca23ab990e0059c52fab332b820b4abbf4c081131b3a31008cda805a6026305fc9879609813c62af29a2d8b25a0657af82458406556bd68001c7721983acd798b05c7972f1d68243b8617160822e961f09bbb609fbbf1dc757a502bf55c54fabfd1eefac46eb64f0b4304f7f9388aad265b644d9c1db8635a9a44933e328a4406f996b05aebd5329388fc9ef18a62f678ea171356c872fa9345df3b345ceed19ec2af616e72395835ab773d4d0aad17f92469577b4f19b872eea14bbf04f83b6ad321571ac6e88545e0fee97c8a33333f84d9b6c159898ead703de5fdfe108c421e5b597a2f9dc30a1e96c385d5b33256a2d4d0ce1f04f24343dbe461c60e9dc3cd9333b305c1a4fe5b73a74176c1bcf2e7bf3af2a6b6586dc5325580363b123229b9ab18ed97069be1b075a1c4aedcfa5b335efab2a28e0f5e61e20fd1dd6ee8b57024b58707ac2f393c4e073ef111c9b46fa8e29780cbf5c8a1e96776e091c2588d2fbec4dd226aca7b6b48050b4ffc68ec093e1c524d58f7ace225cd257f72116a305349909cb05603dbc5694a7d5f80db78ed4a815067c6ef23fe327b3464cb1cefb1bbc8b837dc99db6a22d9fb88a508c867b964d86e92a7a5d4917ec8662f791991f1d383dfc96344cfefec9d520a102cf519eda414e6fcfd5ee39b1a8b232c9c49c0790ac4abc410f51463f07ee9dd682297da404db04aeaa6205771fdc7c315fd95ab75546877c269815e64891f36f144ab9683c77fc88dead69cfa6cfd7bf2ed317e4948a2851d7475855b33e8bca683bf5916afdb701e835888c23f59c31fb8682547e1422b22bf7b6ac751239c4e23fadcf496309200751a8370cb6bf5706f8c9ea7aa43f632ffac4df98f2ce0db081b3196ad47a6e63840167658636862d34382c7f1e657201c18375d19394259ea215c9a590538b82af885e9953ba74c2af99a0e61359437c5dee186b30a7bf4b052240592cef922c5f17520ba1469cfd4b8c6c75877d9a1f47de3a3ae9d8d3f1c9a52c6731aa8aea3700b157973923ff8608325235998ed53e491bee3ad9e82e87fc885f25410721f1af2bb776270f2e3f4ba8d31d625fc42efa87da01419e4f1085b2827c0170e6b35aed8f4d35d66c9ee18c170029e7f510efadd412ba2efe30d9af01927ca59f9848d97a525ef585421b537cccc9bc949e10aa87427a9a7a1cd40235ed324411e547ae5e414882fca2870d1c33f4a54cdbc167ea2ef86d9f6d764b9bd0c662f6ed6e41a54a3a2cf0cfe416097a5011c6193388d621eb070e5f254be24193e9e976c27b2a02b8bcd56412a0c37aff1cdc3ccc5cf61788a431a74be83f87ac82ad3782176ee7484c3823dcaa1e1f6d17624147169c67ab0f5b1515d59a58f1cfa171b8d2a52413a817b4e578bea348c68e2f13ddabfbdc08f4ee72202d9b0c795d94ccf5fca6b0be617ac19a1b10dbaf024d6bce993cec2efe069996022f3991a7e9645c70fb3bdead0ef0e8d5b9e2d5276718a17dac7fa87f2c7848b53dc70bab5b75505e127112ba1c8c8d77a85f6436607c7022e4b19974bbdf0328287571d69276469a2551206acbef55b0f6dfcdbe9a67eb48a2a14664f2a7a098f6bd8eec95ebfb3ada6417cab6361003d731205d9e2503fa2b36b8a009a1dbd3fb76af52c4f1352f5290d4a4d88ee5596b86b4c1a61488d7334e5c9d415e8047ad7ffd55ef7babb882c0d9f0521f8eb1e269af202e163493d79cb8040bea0bf7f29934144221e19a9fcad287f2086e5d13cd113385a0d46f3ecc9bca815dd2ac7b9fba0feb62088387e2018576faf1f5a95cfcb99a21111efbd30c79dacbcf2c9214c340147d17f64de45a860b83198cd395e3bdc213dc125c1d9e935308896b9e8ee0fec4cfa15ee1e7a597eaf523858bbdfbbea953474fe858bfc97ff2fb783138c1ed11688db7c676de485d450d725c7de0259cc1ace5c125217e9e5b1ee966eaf594a05f988a3172f02043fa741fe9bcabd759612c7b49bb4fec854b073d37715abcfbaa59405aac7b2b6fd71df4ec72c5bc53e9e4ac84f5deb4e610222abd1c867ac8399cedaa1be7c5b36d095f3a4cf5d925ee385429448ef82d61db9edac4a4f8ceef2d0eb80d5ccc576890b9047cc9db9cdfad65eccef4d9fdcfaa02f89fdfa07fa7f5e84d4db7b154b55846735a470f3fc13cd28e7af164cfdeacf1f01aa8ba353ed63933769f27d282cc7ae94790daa41265a6423edb2303abcead600576b96dc39cdb5d02cbd8cf29c5c8260f72163834b868ae66c4b62ad447d51c9c2fa11172a77a120de2833eb1dbfa77e41b4cad596f68b395fc0313031231f9775b461b0a00e21ae5adf312c500505c048d8b676d253bdae265415ba068c2e1c4f7b27ba258e7925c22a8b585f957fe28c3c4cbef758972e14c64665b8e3592a1d774ba5f5ab1e3e449372ef93359fbe61488a56be5027cb144d6a9f490d1e17d449f9b917c10d5d68bd3ecc6f77a6f5d9805bb426f3f9ae55006ff18e5cf741600951189f7c7762844252e651085a1d543712466bf5c70df4ed3bd7a8a658145319d3dc0e4dd168bbabf81df99dcac7b382436d929068f63ef771aa323080ec4cefd0f2a40b034b0640ac7983b40ca0f1641ff4c122b472e28d032f7324a9028b4a545e1edcbd62341a9777f4d25da6709744ebca83c94120f401944360ff9343f8d6bb965022ccadb4654dff7ccac63755d153c262431dfacac3e6917c3672fec62619c1789a04b6427d54b845533eddc16e424c0917e6d493a0b8e2c58a8ac20be01e180a44bc5edd4292f5e9dd22ed040ffbe426c2f1c5d707c8ff68b4cad9eee1d285feee521a5c6c9ef429219bb0c69d08c32b41f2694dfd1c7bbec6815cc07e30dc810553266aee5140241aadc8cdafcc3726210223d42903f63510b66f3153559fac8e1784a5999a9b74021e522041d54081012ef16d76725106b7ea9a554fa2d75097cc6ad52457006059de5cd8ef39999497beb0025fac46f834bbbde12e9ce8b63f427e8068a4b8f8b7a3bbde22bd894356eeaac8ead38324ab95b8ab61b3ea964aa6cbf5a08dbd2c96a248c18dd83de67f4333ecf1e5413b1bb0609046eb892179c7758f64b4b383e358b6fdb240abe56cda38c858595b38e20b5b872a7abef75eacf0b10e9f99bddb8feb6aa260d2a222118693ddee203e3a0d9f2f6bb4d49710a02e1b952604565629e881b3cd87b3be9d2f9850be9d9383a7065616699a7a6fb858452547082254c50acaf610e071d5e470522906847ae0f26632726bffd10ab4630d85f00059607ee68547295d67548be249fed72fde62f4a237d1b4d373b8b8f8593a498eb6c164de5cf2485f4136d488fbd238c51b83abe3573313b9db8837626ecea83bdb3a0bfbc1e548dc9868164d056c1ca65f115d5d13e1ad8c4bfb3376283f51bb1c95af9c1c00d6e0f20d5109dc32b602cbe9a7d6faa92586c92d4e42fd543450dd512615d542bca91cb112518f93e7f0671fae4b6a511a9304c7dc6bbdedc0e1a8ba612445149b7c5833596557e84c9f747ffff647bdc6ff8ea4d94edff6886994c206de2a72b3825d33186c0658334cbd9947c829315f753ae7f40813bd6b9f8d0205b400e22e28fbf261fc4573e6b4bbf1fe7573cb01582645e179d783c583f0adfb61edea78db9df8730ba3b75ef5decedbb33f3936fd9a52281c0746dd313b94ecb41854eb096567d2a0983fa28b41207f2238ecf9dee141a6d35b8e3fb07e26390872dbebb6016b25922a22444dd85a31bddc28f463f5ae10cf52c2628a3e60d635c57daf735209bf360292c26d66d695a58a6e0847e742633852abffe9401504998344220676c206829171d86d5c3c437a79ccde737e929163abdc4525f2a8172f2db69231e2fe28e3651243947c8ef5cfbbf9be3655278de8154ffad1ee606c483268ef197f58f5138705b1ef848662ae349a5ea06d46e7f7eac8bfcc34ffbc46a383f553feada1512cf6aaeca65d826ee5d60babedb82a672a7f8a66e6274b033125fcac665c173a0255a955be9ea3ff3c548c1632635bf73f133ae4e1b16da820a144f523b845b9d0707ce39af6607a78c009058f1b200cbba0f0872d5345ae945a50ff26acd3c3149c84589b51ac7fb9b1e691fa3ff659a7bd0fe59fa456b1d4ed1e48dfaff4da7025a4e9aec6951b539aa2f580c5e4bdac37b4148610014647bb43c4d1fc118330118c420b9d02e6a9ec7d7241bafe3131827d5a57233dc097c9269cbc68f071db2c2d947f69ea9240dd89c5b61f5288707cab33a7cb0b15a23c8d69dc8bc0f3ae94d3f40043e25cb1ad66e8df17a94c7c14e2c49f9f57c73f47766a034e9bdab6f19ef489b06fee045f243cad6b2ac5e129fc8e178383d0193cdf97bf0f35f4b0259eec2d11ff404e1b15dcc5278cd2c812f3fcd93562fb5fa22dc30571856093afd3e571ff134474d00a34ef6765693cbb56c4263a8e3d59335f10c18d1b33cacc37ecf88d4a152a17d24358834419525f55b190e3fbab809a2ffbea57eb6e74f58d377ee578144b580047804c53bd36a078a2c867c80fd48a7e357a5c8e1fa01570033c298065008f44382fe6a1d79eb1c29c6352af5ae5944ba89fd6f494e98c910e2ab9130af5ebca29a36a9ac1083aac1eff302c7845f2208bcf4292c18a22fdec81667ad242a6a209c4f066688601a35b91959a37181f13df777f69c30fe5d3474c30b7bbd69c8850a6e95146b30c8768c521de3eaad26abd1ed8f5470b868877d51673d995843d1501c536a7000c8458d9707802bd81177c3cbceaf973963cd848bfeaff5c9cc46c51a2d4f0e80f8ed489b56c7b99242f84ad292de2bcbfc648ec29cea0adf35efc55584bec7877adb2c5f0521e2e2e6df6db46f002033630705b43fa9db2e3d75b6467b12d9e3e7a72a73a33c62a1676253660408251fb7caae42280b45ecdb6299dd6b34cea3be35831858c1666f9bdeca8ece603b5c8f4c6c320639e2feb6ab8237d256a926d855ba4bcd29cd42e30c33fcccfa609cd0ee58fc8b33f499517911fe24eb4e38ee787d257246f6457c9451f84cacaf646534588b5755d5496aa046541baf7af64a7908f0478e7aa61ba8ced05220862738a6a804da8e63c2772ff990f5589dfd3b1ccac139176db43510123b5ea224c50bd6eccfc9ebb39c3263f1678448a313fdfc88406a0e8ac28b0297f0bfe1843bdad89909e68e0403bfb1131940b5527c617eeb1eadc5ba1799b3008c5a4a8f8933b1b119182f5e08de83d3de1b693a8ca15cc4d674d259c2b59f4a46131fcfbfb67ebbb696bde23a25b3789693b70e3304ba95ffaa4f0d73eec6b9ae5442ff28826c1f2780ef19ce548537b46411d5c183577f20b7c45d8b5edc57aa5460d1941abd5921adfc291823097f05c391bfb76bfbe0d88764f7319ff7d48365b5f2f74e670f797f89bc1a4a26f4d979446757a97d8349ccb2ca0c615d059dad8185c45d90f23645929f0cd51e7649ed7d7467d95c5c907c8e16641bea9ed7fa228f8a9866b7910b0d65569bfe277b7985acddf08792516c5b6493d2c01a15472caad4882169664ff1dc92d7ba186d87d3ecf676a7d53faef6bd1994c2496c7c43bb8fc5b22fc295e43b4fd9997640dcc6dc0528e647fed289e591f9065a4c2d0806b7e490f1625c550f3c5a19075c3977f519d8590676ccc96932cbeac0349241e9376d05ba0313f60d6cf424274ddcd22137d295e4c15332e3e0115272480f345666e2786858ad34682835ec1b96b7994c6231196dcfb386ef99a2109a995f15d3322df68e14d66ae9b5c2b5dbee3f1feb1d9acc92e07236a2724197b7ad8291e5205e516f9ad4d33a472919326f748126fb0493b8a8816b0c60f3da6ef4891cf0a6c559b6b87ef411d98b32a7b390d2eb65ee6c100e74da7e18e027209e998bf9359a9e34b0135f6ce59e31fd2c1d6dcaa57908d3f7cc96bc9b3b229bb4783afbd0e951abb9efa0591d6ddf6f226d623f79fdc979f5d94d43b6ab9bd3b417b8c8db04a047db0841469ed408950f9311044950e9448c464dfa807bb1e663d599ee13807af81132dcc880fc9d5b07130e654fafcaa405b135727ec9a5037395ebb8c6e80c1fa635822c374fefdb3c3fbcdff53cea874ca3a4bc155d2a8941d26e207d24f7877378a6d2c58717e8171a9bff64e19497142f1baa71dd479918e57c1db6707cadbb704ac5f28953a87126665e0edf8229b0304fea38896db2f0ac0139e33cbd2e68d3c387649e1118a6a051919959d79ca86971ae1f14f54c89256dd5c683012bac095e40dfddb525b995ef1078f7436ea90448caa065c7314f7d0dc0a54e5087a1730f2b6d33df9ea527267c44f8323da7e87893128273f03b70b5fc9e212cc1e065d73819e24408399818901f518287fe03ae4daafe1ed64702fcc202263ba2bc7d1f0e41a29f45ee44a5e28c43b1ad5e71d580603a6ada131c9658fd31d9027dc326d4ae48f14f91b7133b2a3b5839b3476c075040a904717cb55cc6ff54b17a518b6f3ad02412bec618d7132346d9ce2d41fdb46f1c9cad233d86af77c9a16556cce1aec6d6a57739dbcdbc4ae39757b3d9c350e4e33b345a4f9c5b8d4af1aaac635a856e608ed80b4f3daf5d2cc74cf091b6cf73b0da1b59321a82709a7aa21b4d5b83463f4148607ce1770c7e787bc89ea612da7ff545730a60ab7ccf5878ace9e20a6e37967a633bc28f9484fcc060e2f257c8ddd204288909ea0e6cda4acf59e2416fe28d01c53a852c39e798dd2e0ea4ea92545ff253a8fefc9ec233659954a630986f470f0205c57f07ff11485ec726dd8da50d2f388e2cfe4e8df2583e527f877a12daa2c055b7f904293cb6a104fcc5d922a65f65bcaf8dc3156a39c777f2897a22d8e13b577326fd6b003f447b778ed05420f74884d112ad567512a9fb1c0a67c06e861c572bef35b138075d3f3d38b81add88328c3bfb1e1469f34fb2a0e631f8218d22c71479a8dc548414f5bd817178bb8499ac66cba3b8ca39ce9d75aebb303c206be663c63c11ffdac72ad32c73ec23b0e114fef75e88c535997d0dad57a6285b24d9cf82816102523c9e3c9a4ce95a1b8dbc218166333bf42bb58d0a3f63188a96c0140c4ad8dd7c34319d7f46f2a394d7f27a30c25e620a1cb1c5e7c66f428feca429601a9f44b958412c84cae152c3419fcc457297d5902828c84bb01bdeda7e72f58b15246ac3456fc9c949bd47d6723ee1dbcbf2c71912e6a6de9a856aa883160e2c1a74709858eae57b9d9288dc2bce79ce63f1ba047a6983ceac2527efb502694de29927e0f95d4d2ac57566aab6e6baa75b4f26c06efac128752e47699a85075c0569a02b6824524b2a84ebab4dcf48f67135ab85125e85b92dcf70d2175fa3d8e308521cad7b7b4a189f45f3ae139093645bf01daf679ea9ef6ebfe26c80c2529ccfdb84a9c8f3e20797b90cedde18705977cf469835d0b2d62b45384864361d18373cac911e93a0734c4e92375b8d2ec3405e0e5532162c7e99427207c8b60b38f585c1be66068fa0137590214bd78b9b4927d38e1830d6893e5b842f1a014fc0d66e583dc70316e88dd0986a58b1a79947fdf89b5a552e444963c0a928263e0375c97bee16df100f3712064333e7a6c94204750e57c10f51e553d9a386671fab0cfe38ad0a4c17356fd07a7d2fb3382b5dd3b5415fc4b58d27c016e40b10be13262a846708cf896b540fb3455d9dad3c107afe09e303790dc4ca81be05c81405a1293710eeddacdd84be327df9a52357e2aec92760b9d8680e2bfee757c34bb7810ec58361617717a95c2d899a429ba541c716b64715362d0aedd9b25f85f540b16ff6d8fe211de3db0db5c51d3746ae3b34b92ae3bb1deb32d1b20da428428304cd67453594885b2e1c78ae375146c8e31601f8895be7541796be3d02e1b18d760a531e309750b861d48581137d774163693e92806e27b51b13beba9c0960e7f474a45de218077d0d9bf8a89164b4aacbc5a044e923eb7c70af994b851af4090df31f783280e4268f33907f2baa84126b8c7935de1eb3ba8b3b5dd41d574eabc7c8e4910d48883fa0f949e3033114ababa45f1ab891baab14414ad4befb92f738ffe099f7d6470c2a81a736b762525584aab0f6c9971eea47c70c07b0b8c17042255d779c3187666fe73f2fc8bf58f0b832bcd04a48b12720055bf09742d8b66508b966d7dbbae2c25fa482a5f7fbc5175f6fcf131eac241198eec46a60ca03defdf9ad525e8699877cec9bc670f9323031b3e21765caeccaa2f4085d08a93e1fc09a9fc8aea98f64f0d18f33eaee7ddfdf3060cf282f793e40cf7baef44b74b9f4804b85e701f665a02d62865de590674b9dcb73da8795273a8d7bbc2060932a310d4251c2435caa9b9ddb796a21327965fa814c6550ff2bd323e5b18c8e93f9b67f9abd2ae0940519613d855e36c187610e46935f91fb1df1d5c9a063282c19ffcc79c77aa7d2cdf9778d2ae4a5948b416ec67fe121c0d8fda1e88575ec251643d643743f1a61745bb10aba15e26f4c5e7d47a5f30ec91dce95eca04d0ec12ba13cd1e679a0bca079fc0e1196f35f02a78ade9b54e628b6ef17f259726be949321db2c6323e85de0b6128299ae0a5e5ea5cf3a20333cae7f0d6c49bd9e0da808e3f0bcf314255600393eebdeaa57d8753ed6d2660302f343c65958211aee08893f4e3012a541a1370c22bc9d21c79301c23580cec16d69969251116f701e61f967c15ff9f1f550a1139d4ee9b182a4d69dd977f58cf98627f335c8b53ca7232ba806a84b1deb04b594796af2789f8cceb88d00e09f8978b9c4d861b1f7219e1c7aa3af49204501de4ff37c0b0b66795c82a0d9f58ddcbe2ab7bf31af29cb8c2ab438032e147f40537c3a6b3275674ad5e2acae68eb6291796d739686a93a56834af0a9e52e0870ca4caa5580fc031a8fbcd327d2a20f3b342b31f98b5c2f8b116e3feeff84e41f29b6776f51b3cd0558640edba4a8537b2c3f5917c4b59d932dd7b050447b8bf36e266566480fb6d943b2e4b8230b007db94d8bc12a82233899189d54326a1f9d83b65a6a95f62c4904407f4094cc282546290723130bed5bfb45677225cf792a220307c3c28615c3f98d0f550a2ef2b3369ccab696cd86c9d4d978928c8aa2425bd1ddc704b367eaac8ffe8700ce4138ec00afe10399f6c27dc8fe15cdc0c4135a6d25532b47cfa105201a87d1233bdb1d088145e434b4b3eff4acca4aa607b979ce48aeaf36944e06412d1548d7ac19a0315bb582c083b66eeb5b66f7f1b666541087737d46dd275bb2e87f82bc29a8c8f6b6f4d7596947bb2cfa19e1c88c2cf03bf355e1fdca77631c9e14cf2b7382d8e056114b8e1e4012d66fe0837a0b8d7082445c942a7238c6bcc76deaab0741ca139b088b8fcccbe47af0c8f562ad26e4e0fc669d9cde93080045341c389b77c1334b43fc0207e4f32954e8cbc93f58c72c8ce9de155e87a672000d2260e2676575632d451f37fb88e082e288daaa5a18ff403bd7dd83be04b7ff763724045651933f5a8b5c1a4c19becbf7dbccbe8d99e67729b51c31fa03ff266b69aad4f1037bd96dd7448cc4cbedfcfd840b1efcf7bf52b4783876ec1d8e7131961b068e528fd24013d4640772ff656058fa969f6efec2df3378043c3d50558c99b081f2c46c07ad0384aa046781e9ac9c8392f3258b1f10686f670e1185c1f84b2f0cf5d856f547e07514077dcde78f2e2dd301f41a60e92e8717bf07fbb656206a5209e854a839ccf29a5babc06453fe56d6ec8b3149e2e780f626065ee39cd34758333f62b9c7cf22e32d5ccc6de52f0870611c754fda2795e9b3d4aa09341f964e34ba54dd55a99705d10ce35503c6cf0fe5c4d4a04529ce0b432e3652b84e3bc9bd97ba42eafcd271a51b3c45aab976c172db968195505982892cd2f7702eaa96b72021113f2470376e75df634a645518a0d449893abd71ef7a0ee7760b15ca47bac32438df8cd53dd3395869bf06655d5fc11a79d41d78680ddfa8c4aba613d5d5a56523edf216f231338f5c52e8d203ecba0350456a4a8d061df1b8ca0fc2e8ffb64a496b31c900bae62362bcbe82db0d4bba016ef7a99f382bf3728fee3da1244e44226654527cbd5913356edb8a012f8c62c332c29769581a775f3578394dfbac488469da9502f36adcbae5fc0f24e0b6f65f90aac0ef83e58b1fca4ebdb5d0459726d82661b13e904fc0d028c851c8ca011949621649a02c283d3dc292d4bd7efbfd784a6b04c275e0f65829b2b07ef4baf97d23a49ae1ce124b230e192b1248343075a256bf908154473a4ecf3bb0583220788b64a67f287aecd1a251c8faa8bdfdb8fc28d7d5f6f9ef817c1067587b3d6a9654a486c9dc156ca857d9fed6765e317574fee6f479aa0506c18a1bb82093985f15eece179d1a420f059ff103667d46cd5754e8c17b6fa50d0d3d18fd94e240053b0e24d5558f7478b8be9647862a25d797fd3b911ed446fe46ae6df57a0e5a3fc069eb4d25dc7be836aaf7ebb6d93d26684583069e7600eff87f341ef6121fd8019b3230128ce1b5245db6048da9a1a9456cdd46fcc742549011669700348e4b551e0a82dc0efc942276fde91c0bde8e482e126e9005246e9aeb84efa5746f47b6354be3c486104070bc9b55ce07d7ca85226e68276a9eb809209babefbbc37d7078903f94e2b3d415c9ce85123b670b4039a6b77f96989718a9604a2f882e7624bca2ffa54fb62a3fc0be93df9b2b8f5b92678f01d72a20f1aa6fbc9a0bfaba995ee1ea3dfbd7e4743d961b150ad93c3601c8551f5efed9c7eca756d17f28c1a70253954418b170f76c3b62d20a8e3aa4980b483db62c2f41265f7473b83c6121f7b62d74a6868d238057a54281038ef4be643c908f829f6516581a356c355f8718336429b8371cbe4810c9627c8c5f5baf5628e8c2e3a2bfbfddfc25073e61524d369d85320a45e4472c73c23be738a5bcfe9b4b23046eb0895c202acce68ce49c53f0248b1f45c7ee72ffd1b1be7feca56ab8e8dc7751963d9e4a462a552ea4fc5a754648ab8319b3377cc470f69414ad922d11beda94d220fa0693998deda66b9f150fb2d77d02f831e13c5bc98df92895570fa69bd198cfd20a15ba369cc15ef6a90834fc0cf59d93540acf44b955d39959128fe300bca5c7abc4e58a2e9eaa48b2363cb275204ec8f925e7944244aa7bfa2d46e86729784b7fd24b5d259cd22e44bfc129e46247e50eef9ac0685e9c64088aa073c3b4328346061b89c2887830e86a15a2f4cb792299e976eb49eabc9a14496a3015b61d5772d800706f6491f70266041475f4e5d075190d9246445850cde626d0d5a26761cd3078a66836e7057e81f239f54795bfd98918b2bec8bd697c2ac4e46e1610d150843e62c6e0da3721ea064c083ec0a52b321004945960a52d38d1bcef7d2f0072ea8a8f2bdbc00d3b12aa48a434fdd0acd958f137d17bca5e7265a82b940cb1b165d4199766b0efbaae612e012b60d7cf0661833383eeb940c690811dd4a6f4597d33d330c956384b927fdaa052f0aa9c5b8661a67b79ed719636a10213753e4cf73ab33aed99634d0b7b69fb9b1cacc50615863b4868ee23d6b4e58a42fbf2ac251c7ba7d66748a5daccaec035f5221ca5a11d181437a65f38f9369eb149ad99976efcbb2227e4c7ca39c7bdfdf6ebdf1e88221402698f1ceaa53ecf2d2da36484e8418f8afadc367034e9d2887198fdba6fe3c0da59361346668f2ae69885b42393eca7f02a1358a41915823a063463f3a07ac9822759944f0021751294899c03fdf06c7f9b54ccd5e866ea24608341f70c8386fc889374a156835135a633a0f134a2ee5d300a1265f0083c47eb1fee6496d0bedfb571a5afec300a230b989c357cf1b95afb2731a543f4a339d53071493f688114650d7fd195151812990f3a17e6ed7deb88400d8439bec505057f460c97fe84e46e3664f1f67f7c80868ad1af6c80a0a65f8a31faf1d13a6fbd12538e736e4a4a1fba53c42844118783d85e72ace193cb8bdc4d77aa3a27c2773de016288318f4a27734cc38b95a7288248c0b2adaeb2fc4967890137d8eb9c9e72fb31fd7e97a1728fb1ebc1ac89b9bf21e17962ca0fe840bdbc330657caa868ab1f117eaa2a6254483c58a0bdd0815c930806ddc45fceb26cd93b7d94759acdc4f814dc34b5df0952b62dd75a93a660cfd7afacc4734a2f1239a1ebd6eae60e692930d25319ca5b51911a62d1ea9ace1af3ce815cc95a799361a42475e9d8f86880dcef1735c28673b36727d10605029243695d1ace00a643e180e3a4354ba404a85ff4af00f98c8e23a5edc572c7ca9047828e5097b4e6d53711dafa7a238d00322cd4ec0d31efabf72f05e0039e40b788d1d709de794ef15c250950099dff86e23f7b167d9cd7ddcded66b9a0680f8bf750a0c23321e422405a67ca04b0e1b7e0b0ef67279687b792b44aa0338fa66ffb44f70a27cfec3775c91f1981d375e71f2fc823ed4916bbfa532943a7dcbde69073b9e34b56071bfff387c95dd90a5b73d9e012efc34b57b00ceab19ab368643f981529b29776e8b38bd7a1b32313f3f363d626b4b9b62e6c2deeea9730706686600b54f8bc52eddda082635f4ceece7928cea8668993c75cde07a15bd78e8c14449971f78896d3697e4d37c0f3a5d8161989da60d655bc33132e55214b08ff77d82bd6519e4838609430aec326e715ce984792b567dcf1f26ed58a53699f79b428fc10f360369851708d744731bdf940fb73c76508f8315c532aee532732edcd7f20ba0cdd380c8a28fdbdd237be74247b2bcc5dc930eb3cb37bacb742d6ae0a583dc28811aa3e3ed5a371febabf88a103d6cde3b92b6195e23098d7b14a342df85250d3698ee77b49950583c59647cc560403dd3fe939e13b3bb23a65ebf44eaeb31856d7fda74de196888468b80430bf1ee885da4b52f7eea05cc40b3185fc31f8d65c7b6daf3bcf496b401653480618b4c5350fc63453bb1cc16361dcd09608315e1f9870cb11662588c53609db51e340d756b08ccd16364ee79af43d94ba4712287df4b67d6a3a09cd2d49b396d4db0d3bc26c776900fc887a21cea4684d6027214263ec75c51bd7b3290403d74333f96c31ce54d90a4d72fcf6319cad4fa9f0634cf3e4e7be9ac593ca09f078fe1a6e4242eeff3d5fe5158621532c38bea6e2ed1a60ef9e60b5c53ed5a64b3b3517cf994c8998ec6ce8a9e9ae477af2e3432c53356a46f5cc02f4e3d4b2c5c94dd45e73ffa8bf70560fb07be46556877ecef3ee298fbe05fb7119ae2578d04fe06eb629411201253c0601ec5e63d82ccc17f5d723db9a403767f8cc95f40f75144ba62da2f0af6087e52fa0b82db530dd7231f33bc1cbd18accc679f55fe776f855aba3f67981cc738744defaf3a4e783c0b5ce2a924974715cfda9849b7b4dad6fade9b5545213e76f52746cbe0f95dfe1ff20a81dc69bde9d9c07e1c26c7f5990ae74663165d09a77a430e06e864d60445223c9e3d6730b5d5fdb0d5b0dc88984d86d15701659412bc9a8f58ef95e9534cd2d62b8faac0fba87ee8ae28435cab77ae5c985831343413b8095f3928b70e21451e1f43e36b1a6e0e6e3fe4b0d65792ebd032bbdf7efba551f5ff551e1350331a46849b1e210761821fd185221e2ee81aa28649593f25b5ac2a97bc822c2c5ec0e4e91835087e66a9e9544958d577696648b83b730d72860fba49f33f1d843190a878c6fa182dd010684df50375b22f5af09f940dfe416a0e161dd5d96ade19e82d6c355471afb0b08ec41cc3b01c01106e8e8017e06dca26b60f5d72373f4cafdee45faa4e1cd29ec6f31eb9cdaa7f1a63598ee9d0dd3631e10d216766636d1e5f2dafefe624b6044e5d506b4e73a96d936e8b0b306236a14570b6181a41c65498a0b2cb3bb0ce0f2e8871730714580931f429d41f50f84c13fcc374bda006b5d1ac8d0c33cfe27b85e668d197a72c66eece2699f79c16d1da384585145504f017deb855b8476e147610096aef10c354385713bc9b007e4ab2205cc4287e08a69dc1d2d6be457a5b06bd6bfb8706420a4ae01fa41031004e6a700b84bc770ed2b58c2c0a69b7604bf17bad4277936cc0d0e2465105d9f275a80fb11f576561802c79228224facdf214b3bf45fef86a55ff1b09fa2fea963393625fb9ee0bdbb6a311e31e4a7f7ad80a00515ad5448cfba86c0634e9a20b731a5c7faa56d6c5c4333c72343c8c2aa4b161f16e2ad7d2f8e2060aefae576cb41bd7f885346d34e0cdeb8c27cf435171ae4a101508df4259c5685d653ed4c5c3112afd0845d6e35d96f3ed07cd8dc32149ee94e5a2720f07a38130373ea311e08ef15da6f3704ffe0c07a3fa59813f1fc2f3175f6fbf181d2e3f3f37c3c90d1f63be74cf45d24047284fc23e0e688d6f233f7d5073571481666aa9834ba01000fa69cd7e4419d3ee1185a5dc90c4c88f79b1773823e9b41bbd06bcbb0609478a1e61ee5adda5791af7e5cde1feeb3bfba68210ba2143ef35e608042a94352ffa1581262627842c192f9740242367252e8e39204550517da694a9956e5c82ad30df27ee3ffeeedd44c257d6d24ee37162ed5cbd1403afc1e8e99c88abcabd39c9d7df2819e0d91c7df96cee12d1df2d85e0d4e45b73b7ded461ec003ce53e77bfc36fd9dbf74be01deb63451ebc1c63f3e7956b695cc464793d25e15b6b616d8485547a6dd1a6a6f6f5743ded8cbeacf49d74a73a61d89e72f12f9c87cad1d9cb6767460004f730e950284f063e7d50dc652f07d62c16f7c93ce8a3d5227fe9d5f8a2a07a0ad34c61e43470f636b5a99e5fe9ba37ccd63e31fa1263a7d76a9691b2d7feb7c2587ff103cf3f07871612a99a8ed9f7f0946ba9efe5d003a813d3fc603393fea389b27868ad5eee8970c3b4f5c690a0e7760227e5f431e4b7ca53b111ceb9ed5be32f3bea1127b3d268626cb319440a3a54edd8dd6aad79c49ce8b2ba74c73eb281200ff871e4cef6e0d37f06615c3e15cb3bf06ee10d8ef672ca7f04a641c45601d56512e820e1866f1562f6bb259ba48d7f4c629da9a27c293d79cf194fbe22c9b26ec1195f08c4404f3184c1a3a9e032fe30d84b79c2289977b17fe0585cc654f17350a38f311c610e04010f58c26f02f1670b90a8c765af261c1519a7c5012f288264ff5977fd3d9b707232e7c55206eb1a18e4c0debc53958361c844673d85a98f294792eb9f90c3e65182aad2c178e06c82b39425eba1f981c14a0da31d3526ce57043dd881b4185c6157105931a37bf3f93dfa08276848a83e67a21522f10ac5d7964286ff9f6f5d07a9513f68a3d368bce9aa9b71e5287137923e887bb189c6e3fe8f3a1220a0ecda36e97419ae228a4c861a8b362aa42c01871e3e8cbcc391a4a40498afdfedee62db04ea374ac8f55d463a5723e06928ed4a9edb6ad34c53aa0d8f41d026b9d78f7cea45817fec4f7875e26547cc7d636d1fb01a286b028aafda7475c0512a6a2198eb409f86a1f3c11f343f2c0cbed6bf2daecd47f85119066c15e6e94447031b94b4022ee24bb2c58ece988f584203134f6da3e5b9e31d4bf3542faad2a93f4b21f2368bf970fd34a790485bd4c016287b7341700f51fb52b83eb49aec7ba498cdaedfd64745b3afcfd4164ffdd95d560fb338b4adc09ba9cdc3070b3d2d6cd50a146ca7f73cec3aa53a214e1cec729f080ca31996c79b5d29224a793ca6d9c7fe156cf4221ec103c7f581318781de4e3f53a4d913694a317c2e32f6f34b39551630c1d64ae82d17b026256ebff2a02795faf316a2d05268d22a14d77da68a5ef437fc7f72a48a5dbc5e256037d79c9a93b776256eb43d4183a27160f7dc2efd94f31f90dcaf9fde7c309458d26a0c665e5b41ab1c2bcfd8db300fdb17f738d6fb77a0353748b4cb452ad8896d4a716333db31939dedd3a6033d08c9ccbe608c9886cb1024c6095cc6159f544791f5a39e2c08c6ce4110e13553af016a247debd952af762b30ff06969d268b40e803226e1690e1e18445d5b6df38353af3b6603486538484200d35b8f04f0398c2190086845724f1b673cb6fd732abe6de8a0bd463133584359b92e587f2f3af5e5c6a5fb295446c0e011ac83fab375b03bf9e006eeb70721d2722bc80496d484e848878f27dce78f8922d4421fc2ea9b47a17dde187363cc3dc674ae17010401c372d6184aa725fef991d0197b7b13721b64f41e6d10fc8db1eeecaf9a76b54568e48fa5f54b52cc14decd5c7be8073ea22cc0a95b413337026af1f4e4809c5712aae351641dd078b6ab6ca69b016ef8c8aaae5f4aba8f857e59a2c7d3f2cd611b3ae17c365e0ddbef4ee97b0200fdb2e9f2d4c9433f49fc307bfc43906205c71007abebd65a853a31ee110f1a43991725770622a5980c9226c1c33549b3605e4c95693ede3b8c18cde0221d7fc2fc5a332a6b2225daa4b7cbe7e6759d93539da34e3232c42aa977986a2dd6e52d42fc497157a68d70a296b955332944131ea80a912f93f7aa868c6bde15ebc5f630e658df15b4261835fa937d09ce20686f9a0a2d02a7c1ab87cd6e1ee0f5ca2bf501044253c152aeac25cbe15429409f2d59e541d45172c26f21d201bc84d596eb6abc0d2b97ac26d4ef8b94b54af33e6b2f87afa3aa4b38666b7782009a45c2934f635823080ef4fa3e1fb0dad01f66a66ffe2f3ada1f796ab425d29a690841e0c1e9e02257a1774645b6f05b9d21acf2dbd1e9bd2138db7d7eb6ca550c5bf6b27cfec15cdf09700834247a805803ec625952160a216e6be7a3ea1c55f1a4a2b6e252745949821503a92bf4710f193ccc3ae2dc5e5172bf34fac66f2b12374d8318a8626e204089dbb9d913578f807b2f3b1c741c57355332088e42011fca65033d54942485c6a4593bfbc984fa1df59bf4a3c44c3668e1cd58902d9651215fae9e925ac01416446fe63fb443a24697ff20ba88415df1e2f1f6d60bfe31d95fbdfa7e191fc79cc957940f2cf9bb3695cf09558bce10f022d3b55f44069e6033c3a14f14f20c77339b81e3bae397a1aa0a64b66876a3cafde5eb08e9819da186644761aa7626a0479b5dcef0e3c05f60aa5d717a1dc3acad64a0e75867ae20f4e9e1436a4a870622f077ff365345286be1bd0cc49d0f742dca1d94fafdecf17b30a8da315ca9f73db4115910639c273defe41bf55dc0d62a50012bb4bde4c4c186b8667f6482f92f06a7c190bff679de36175522f1a6742b7af67b4f170a76a400399a2a44c658f9ecb558f283c941bcfc0e53b40ef75d5bbda3429fb129986ae38460ab5ba56aad6f926d8acf2001450a7a147ab64ad313e4f732d4e1b837d615fcc186c58922bf25f3d4796cc6e30e577b7b9218d0fe25fff9b434323780383d589f38b24764d79c6208701c88e158ae7af964ca370dcc6d08b6d5130b417ac5c79286fe300634637a06a2c7c30f222130f26b04ef7ed9102edaf0a24143218a48973b3a5618f85846afb4f6332ac7c395f439a2d5db5acb11273904f7398bb296a1c640f12cb12e653e92a4e8232636125dacfe5f51387c2a0b29a6cac9d191946ea5398c1372c154aed314ca655f5e88d8a36c302e893a5fbf1f7cf13c228e306aad43d92c69ede3d057e01e757b026899ccda2f06b1694c86eaa4b2f128d751db4b6925dcdde48300ab32dd24e54cf58b36b7b2d4682fec75c33dc4e2aa1273794188f6c44d4c89dcb879adeaa564cb4bd4520489d32721a5c50ec58c1e575ffd1a39baad782fb1a4886d9d8ed6819e9b9a6d1d6969afa597827a0f7fe8d0e7c8a96ce12780c6c034e2d1adcfdf49a6f83d957cc08eb7608932dc96dd9017e7421b03a0ef3ea4623816264d5ffbc6603a7d5949e96ed16c5b0a587dcb7356ea0ef5e20591903c1137418a4a44cb341def246a5954aecf70c9a0c9e7db1f3a322b22e6de89825752cb7d2b3a3dd3415dc54736eabdb47b444eb79425538f3f2bba6d12027a4c41fb5c447325f86011fe399a8265482f9cb7ee637b79c65d40e3e2b92da5c44af943fb4011c3151b99dfb2aaf75a4cf11835ff4a1e277ed704247a557bff640f9ab11882cb297bf59854ce344a02c42ae8fffc9464677a19648ee6db4a957c97dd108f5736e7d8c543e7f0cfa9282ec3a4e9bb6fff6f8e977014372c221d8319463a61e9a025022fbaa4d8e16806cff02dfb05fcc34ee46111f190773c3eb1ca8b7994310111374e604fc3a9c04cb0935417eb357f3e4febd956b955ca3e60e6f88f448c23c3b4e555d3c354111550acbf8889c697ea707694c49af09caa8d3c1b46e7933cc3ac15afe6e69a1f8612d955c8a3ba41b46de0853a7409beafbc7dacaf95eca214c8e7b5b8c3a8b277c493f064dc41966e95980f416af5aafa4a3d1b8c337c5168eb99398c73d01e88e069e17fafe2986ef18678e6c1363aa2f7ecb6c0687a531788d88aec5214d649e543bb007ad76cabeb1b73f7632f6b036b8662cb4512b1da7ecbbf8f1010635810bed8d798a2b43ecc9a54dbe86248407508161af3cfcebf4477da5996c92fb60b59bc9f69943d8c8c4d40034586e28cd232a7dac9d9674fe4482d563236a17dd7d9141472d616480708223200faee2731d776826ac9b993f4ec8c2c3096d837d383c5f0102d811e49f9ee0eed361927ecc119d53f4e61a5207bc6bdc931c9b65243882713fc4e2320b69ee9f014b4c49dbd66cdb23094eeafb079a6f959a0fd24f2c675dcb9eb64fdfcd584067cd6587f58e8053a2b9d59eeb098013acc4a656345c924b3318774f9e69406bfa7f9c5c91ceb6d7ddd605993c8d9f7406a78930c95117a6830ae8c340b82b90e0c3e83446343fd660e1fe9842ef74ebb3fe86ca173674729d1d27b5caa1cec5c33ed237e7c93a7d68dc0426fbf3efef75e6107cc7cfa216319b9b0ad0e27f6d4f60bbc2f48e8a9311eb19310f45f8c77516c5a1ec7a44317ebc9f99f8d2744d70758bb88785b55c73a35d24107b287df0f81c863c016e2f51ed463dec27d94e8c3f17e3468e275aba2974d052f06510ae65766fd68b8bcb72835aacab8145afc5eefe057c1490f52c696230a91bc7a8e53fbb763f769442a6b8f0283daedf4bb309736ff9f68a681a9fd62051fea2328cc719fe318a067f67a8192ca4eaf09121d78df39119d769286ca5f5ede19bcf22dfeca6b43e324e6b325c9f0322225ea64ef976451ee5938e3a7e3d9aa5fd6b59ea87d0fc537ec5f8439030c5b72d0b828992255fe92034ffe12655e3725c406bcc9277174c0d044453087a2464e9fbb8e3a5991469488f0940b8806f30562e4315386d4b76534fa94315cfba37f9b3c50917d02b48049417c2f43b16a3a5f8b5cf6ab94ea191b56e1a288b6feee15a0a2361694c6883ae3b03b84a8c384acac11c9174f84eb57912063db46e9b00b6cb35bf9722f69fcd9ef120a24413d99294ca0347e1ad0629b729c9ef5df802a20629ae129617836106dac057255fc4e1927c27f398aec815586b1424d992f7dfbfdfadc51b487ddb5313bc25032fc2f59ce34b992bb3c9082acdf7113970690a4cf7d1e18e0b151d8ec352711927c97624f610cd0d190acbd8ac2bf134a75d474291c2987196d4a52eeaf9b28f0ca3eb13b3e4c1e5cc1bedce657930108b17819cc8b586839df37d86c6bb92695fc7e7b7078dc2f2707373656a76b79d310c130d6d1fda64db78946a1b767185cfd166269dac3051cd6fda90c50f12a18de04c11248e4de84b482fcdd97f2971a514212ecfc9471cd7350f766d514cd3ecbabf279b6898ae42b5a33f3821b89cdc41cd5317ac37c1f5fe32ecf3b3b907a134aa48e03c1a6a1f35cbb15d765eaa828f73c6ea0f3c509a616fa51f64b8a6286e58961814611280dc0adbb51605f01631b277acfc4552d11f4ffd83b84f2b15462ddec9c7433be43de195339b5bc059076536a4016628c460e4b06e2e710d0037af422e3c4248492d162e7a94e4be388e6aac0900374ad27c127f8b23d7abedeb503a5aba67f35693d520aba19bb6cfc21085b944c28dfd1ba5d70a03dfae230ed36d2c44cf937268ec7ea90dfad3db714daa7260e8c87822399c1629da945d9875d7d1e220d7ba2580c30c6d169cce9bb7ee5ce9454b23e82775d63378e2dd3b977eb1a6e35132260b1f48bb411cdf5083162967bc3def5d9fcd10b7ccf269b3b47dc5bbbe70c7750c20df3247b3f0f87ff6c72fd42814930148944290e022b8f6495bbde50ca0f119a28ca656c53770656db8c3544ece4143f6b7fe2fc70277a092dd16c267743acb4daa6cdc5af9cb9749a8c3b618143dd3fb8262b1613ae9043c7f48f7e357f61b4008725005ed65a233e4d04d124f87113aaa3019d3b95bd23d0f0de35f5fb4afd1ea1228bd45eb4da840923b394353e66bfdfd8367e035af3ab3e76703f025a602f4f50681db8dbba219f42f954bf751dd584099d6897544f48269d12eab980c9c49988fda19a15b004e9ae40398740ff859a5532617f032959c0ccbbc1165ba9c814893d02c8e8ade52e97ca18ff9a94efb2fd5e38da57dd35d8acf289c47065e504d78d45d5a103d298e9a30673571ddf534919bd9bfb2a63f4d0d9ecda5b8ac044677fb922f1cf124996a3e032cc234b16327b1523f9e60cd93bd07d6ab63ae161955d250d7be93ea425e12684648dab9fea3d2d1ef30256f14ea01512dd47e1a0beabdf92b76c9c1b60c1285fbe840a230e54e1e9c965770478d234ab94489ddb01d650b61ccb07db9e59038ed2fefb10161512adfdb30192d82d3fc7164173a5846e79d572e3695ada20a56377ecd83e060dfbb6d930da7fa3fe3758d80de7b736c1ba902a1f10b70a4fd602736ab39983828d26c499d68c4c4607a79cbf17928e14430ab7f33ce9d96bb1aeb60b4afa24f4451de07d470777b3b58ee75ee477c43c0be90aa796828a30c6f5a1ac9bd50e3ba0d8cd48e14a7b505ba1f94cb72d41c4230482bbd310142f3bf4a29722deaf0358b8c215645ad042c36e88f8e0b7139191e920ff9284bcf5f8136480f354ce1789e7200d8b5953488b3d2f1da20be0af1740015058d55f1eb0276fa628d477ba59d0fd4d72b980ef3258822bcf75d30302d0bfb2a80c228aa14a2b6bb7ae16b7c45344caf9a35871a8dfbbfc1bc21ad89ec20b83c116ba0cd832ee53ab7c2a50bbc45e52fb30bd9cb90d6b4c4cbaabfe57dc4161c94bcf9cbbfa393a2ca2298bbc5d3ba1e37f8f0d1ed11dd15b291dc87dbae330c4a43f8a87f7be8ad9e9a1f9f5c9bae98998e2576e2352a8bdae3c09449dbf19e75e4988b7b2d1370fce78d6c14fa1cacd4f8f245373a86f2939e4b2445ae73254a23fe86764228ccf48f0ca2eb0b6baec08a2ef2b67954271fc99602d680826dbe4ba293cad1a30ddea2008043a5284c205f43f4b2cb61204b29b1a3441874facf7f9e3ca1509d8f4ccd52185a9f49dff8a86bbc8612a86c34bc460405ce2c4f9bc5eccff64e0991074c7339facc644f4072cb8d8f110da1be8fb279a5097b031e7197d60e224eeeef97d151ea099f5e17d28fe128754fc1953aed06935656b2c201114256bc7dbb5e18a7d1a95f555e7955947b4cc7af9afbca5d817a26545f32a5646ba6b81967e1336d62c60f9fa788e5f8898293940e470c608b132950f48e416fc6e91c014d3d280dc853943466006147f54c6a5c920f6bf9f14f99f295c83760a383235b27bf3045582fe6efc45120d0695d9bba126a7abb8ebc281945016f5c4090378cea06083eaaee64bac8f57a419c1367224bf6a42f0258f151af5d4ffc0b47cda7817feb9cf140de50101111f3feb2b32fc85903f712cb4116aeda962cc7e1bf97343e67f61b910e4429c06cff98f7e5f77594bcb09fa41532eb68e1bb642ae6de0efee059f9c840ab2961588b0d8994b65a703959c90ea20e40e080e8a545efc0728cbbfd0d30963da0ac0b56c7b3278ec7b547793685955daab7b50e3d3cfb1922234e929a089098e0f33c4c621c01ec4d3ace44e7aadc16228885c1843c07f4fb912ea17c093f282717e3b534e7054c7efc509a00cdb14718f304b0a5d3b151fa8765ca7792b58b536fbd983ccfd7bb2520250021d57da7edd153ef3a0f092377454eb82cfab84c1385f6c13089490057d9bd46d04e93876b4724d81a5b4c9207fd3cd2e0094b648e6022e252f9f4d7a5dba5ae3adf35193d7d0f602317f0ec5052dfebc9058b3566b7fb6b8d574bf0c0497d251595644b43fbed5ba4cc92bfe90abd79ea62bd85b80f25efe3e35aa71a9666b1a433142427c8c133102d451662ce8361a4c53c256f02a375ee926ebe92c38010f74e69fd0c07f1615cccf6171426b2f481427f5b04c2e75ce3b58f4d5d87845a6a2aee6e46fe899ad4b2da00334bae5d6e963ea4a876484383b33bf3fdbf0e5f3b3b73b63d4defc03ea52e7db00bada660242b591cdba00aab724bc13c7f5e105070bc720afe7868f91fbaad5584a12e4e17c4eaaacc043b7edc64c6b2f57e09c2e82d0402b22e5d4f11012fa07de3c32e187ef26376cc72825779f909b1515510e8a1fb7a413f14d74a541e4284e35867b5c9302c168c8e36e64b1fc339740582651394d002ba670c55a7b28416d75ba610c2286f708554fa1b53bc2a2e0e57efac2eb778604e3b8de81e93923ab9a9c1aa8e7ea0dc4ddf6671f93232fad146eb6212d54c280ae4a71f85072ecbd12223d8dee4bad3aba7a01eb04430c2a4847b02b951ffd95427097437d93852921dfc29d6f694b16a5caf17305bdda68b9342f707ecaf45b7cdf60741aeaf753fa53cab973cef1c3b5927b3014bb93ea183fbcf99417b0afe85ddeb5930df12160c0b9c0d07b1d257e488a1e76e92f073b3ae103dfef6f26200f6fbf6ed041869934cbb7558f68a36122b492ca01cea3b1b646b5acccad820b2b7c61868f043257b6a8196a9a0a37fe95fecc12b95fbb2581afce7ac1c310286fa0e9ee02ac1598b6e2e2dde0a8032350ca3dad6f236683a9584613181d5048298626fa4cdfcfce5bd522e25352769349c6177644e6f3ac92d9e28b4f676964deaa26ac8dac994b094a8cabd3cbf5cc16712627999479ce3d26e9c8c00f1c1f659754c1b920f698bab169859b06849662c627cafc884493d42961779c3efdba0a2f475c4a88caf2f9eeeb7e7768dfebe71873beb8c06104cac29e7c11393d0d1f9d7cb2556c10a25e3506c711735819af73d8bcec34c6ab2608bb1a39637484b850a96138971d8835cd6ac4094db0e714739d9e911380d09a40c7bfbd9e892a37087e8ad4e32c47a0cfb45a1e5ef48072f89130b3cfb9e28b625d51fbf548feef48df004bb5dac0e674290fede953be425a5fa32c101ed103f8fbfa1a9f53ea15a92530970fece57f2ab8bc1756a50f20ceb747c2ab58feda3e87ec78ef781e0ce76f3725ce04a8199c797d4d1d637fc207876ec25e151ac9c726bef287c0104c5aa33477ea1312cc1500858c032cc95490c20b698cf53aa800b6255759466c8f42a22d5474e42c9e795ac4606b12ad9fceaa5d631d7843bf28474c46392ceaccb7de3e451a61b00c293f2dbae996824ebb139c1d811d67c5153146a449fc9c349e6a9a678f4192e536ca438e38f18f7357e4866e0eea4753f1e9185a6c4b6343b1425506fa867b078a00753b18025cfb8292e96d2ea0b8b769a780bbdb64241d45bb34a5630d6eca387642513b05187e64fd134a96cada0e1e3bfb05b63dd4bc388a6af9f3409b1dac9c67483a74cd553057128be47006c05f1e575b7889101bd5d705f7cac7308f060e133883233b31fd8dfae355cfd4732b22a44fd38e6862d2aa08a379e5dc14b1efc2a3b07358aa7de95045dc703d8d9bd25f78c9ec76fbced05597b0b16dd9673b2fb0dd961fb6d816f27227f82fc7f34ef80272b4fd57bd2589663e88e2054fa1cb7bba9b032dc5a687ce4aeb93b942b510c798c5024befb8035183213045804949906d74ad2e603dcca4631308e51f49cf0bb3de91af0a9b2857a5a65e7ab34932a2eaa087c750d4ebd74b38bb85c97e7514b55c9eccbeaf4cc988243efc544ab912c83bcabac495c3efe5d51c7717c62a82abc95222b29b5eee6d5246ecb591410542161a1b5ea6b7037da149866f71b6069b659ec74c6752ccd0b2a5a21245897d71e5a6e84111c8fd00d734fc8e4ef0394e6487dfe5634171d417a7b5a5e8a4aeb77b2cd178af80f4d12173c44d1aa8d41196699d350230537acefc803cab014c24da95b55b9bfe36aa7b57b16e6e681def8353822239241206d74ea7940e1a91a6af7d5251ee583852943bc5b1f6d27990a1279ceda43df505b2ee5c81b9b76c62da4e04222b33958b514ef34bd6fab688b22332d27007ee78cad8a605b8e88a9ab2233ac24840c9247e1ed220911e386e09ddae121fdfa43fca83cc8d43e9d748a578a5b6bfb0735e68288a7c69bc086e264b969b8cb29843226aafc2eb170b2d6a48d45ec0e6ad04736c5841e5a15de23b0dd7a95b8a658331fbe65b29160bb4b9b64d3f351339d50ac83c6e175138bec79b50da38d1b6b437d7325c7913ec242133af326cf27795ebd7783afd0d5519fe71847c6a4c00c2d6b1e661b3645d5c9f132f0646416201bf3ea6f455aba1d0501214f2f135e39ae3301a86287d36f50f9e3dcf44b8741915fc162b9bc10508d4468b3fcfdec6f72ed26859913ae4c2fb485fb0a1c41feee9ed717bb0e0ce3d088fca3362ea90cca206232a7b5f1062e072345c99a389d489b38028a09a08e3bb77aaee76e7c5997507312573e9d116a464fbda738e57ace4d1b78c0334db4a2fc08d8f7db222b1b361df6cd82d090efb17db7ec020776e8826f6db4bbd04d1822d203d678cccf59a034cf8150f9b39b62c1b7fa7554f1759cf9eb74aabe70d83deef1ab41d77195e42514aa6018b6231d3bd9527b890bc07d0c9884456112b55bbc51028dfae4f8c88ec34c864c60e85c5af27036732ab96a47e735278845ce55da2b6e8b15f82b5fd5bf40f7d80ed315298c3ab65d729c1fddd2d6e136bcce29c825c96def566f01372a311c90087cb93728f377bd49d4141d991f3fff056abff049521df340f83b8c9f250fe08da478ea1aeb50abb10529ae4721a55f06e4965cd03d9486240fad3c0a0445a0ab3fd8bfd2c515274efa9d711a2e44b0fb26dfe528d3045ed937c5b1bc997a02d85f1115e95361ee19de709a99c63b3ed16ddc7b4b8ff63598d5edc5b53b473610d677f5bc5b3e54aa5260665c01d6113a384b4ecdce2b0d41dc8d1ac663a353dbe852906551b9cd595efbf6343145e3681a23f7f86127e8ed4a58709ff4a3625fce8b21a8faf2cd9f31344d4f1bedb2ecc7dbe9d6c8d025aced622a301f2a14a829b4623fa79773223c43aff70493e6a993af305c196a9c5a33f3a0c00dd4720132858893725b58428fb2b36f80e43dca3f19d634b328f6416edbebb13c828edc09048de8cfa156ecfdfdcea99928bdf3d53538d67b64a40bf2757140bf6110455d1e02eb7103a46c246f4bf5e9818e6accea9f5a5958a4362c0f9294134c7b19044258318f02db258ba9f43d3a86cc8bed37bc7de3dc94ca06b246b237e1b2e58d6857f4feaad589168347ed39cb5c6c83e49acbb7dbc3cd12d261f0ff40bcfd6285a6c409b94fa42106371c74466740cdb4585d044a647da2bb6360f818839da89f797fe38f8e4ee8bedf99138bcffaa80e7c7520e5bb22c203f86dc5896c53545a622a7b1f50b612b01fcea7ef8ca243aa3c06b2ff4aa398d73ea7697c1bb76d435c68f97b414d9f0494eb5b1511c0eacbf15e9a0c20ec837e54783fef06a767e1441ef92902403270aacbc3ff14b4db7b0d039b3a97901f20a8347ad01b3b5c1a396253da534ce6dc7a98d09bd66e4b72594ea75da9fb3dcd2dfbdd8d211b42c1070b0e5645bd64fdf401405b8a60651ac4024b93067eb0833a780636c02aa404e661f7d6d5a6bf437fee11aef99b624d8f1a91892fee8135b1abdb22406858fd59636e2ac0af9be8231bafad291f31ac6c1b9644ff920977ed1b542b759b882695c5d15f4b4280f0227a3e5f8aec65ec7925a7e6c87c7f06420f7ec652bbb55bf6cec59fc1ad517bc1c64683729a9372e0b9b09211c7eed024136be0abac8564376f530f5f82d6f8f897f805d12f426cc10af81113b5d59d748053e67f1dcf06202e82d2cd1e5bfd05f450992d538226904ef25f739a7e54ed9b305ad7e3a977f3a265f8c430ae6c7d2d925609935dfd185585f8ccfa2ff97ae4c24fdf9d13cc30bd228fbae642762f294ccccbf444be24b1cd7195857aaf6d597430f0c6cee89f8d8a87bc979328dd0ea03fab8059868df6c5546486ead68de10eabec7a86c6bcdeebba8444a424e8c007c786af8f34c89e571c3be2de38220f5592615bad47528aec446c37dd0fe9285627e2c96bf9ebf77ce96cfb984f9b723ca1b11ffbaa73ee42b12a286a637b485b6c7a9c6443c7086ae34063a08d896ed51954b62cae86d33defd99dd6ad4efff0372ab078f0ab94efb162cd3e4953ca1768e2d9d4902f0eae98597904dda7efacfef4b3af8ab46a78e1c248fcd477538032e4d9f1187d48f28345af77e29e56aafb304830a06c06db0b2c27910a46e2968ef966c83092ecdb169b3620d72925bf0816b16a0727ec172decc84ba5aaa22e76b99aadca804f13fd744bebdd96ff6911c4e5eb0e81211c9e9f2841bf5111ad7e55d45344183839ec1b259071649dbee7912b5e84837e701d224be7b2c813b6a681529afcc603bda93df7ef89224caaf2ab3e948e28b43269d281b2366481ebaa7167653d299de3a286664d6d2135d95b2c82b46ba2a05d8aadd097acd05b6e67c851ae2e47e57a9fd1c11ccd504ea1201a4dfdce4e1d888d757437aa58d4a96d717ef2aad7c27c99899aec40926335151935482cd0a898da50d6dc1fb6079cc6201e179e4a7a3439dc223594438b88a9cb88627d738e0d3da838f04de06934b7cfe4ef2bded30b7483cf383965e542215523104d2777c87005befcd64394a699e71ee41526d24a062e5cc908b04ae15b6e6c0d4d45dfa17887b380efd66a7399e92ddcfea8a3b32577832ad2c0189069375b3be0848b014128824c03c40024913037d74dbccd1b7aabf9fa8a05753690b5d8228c3a01c69246b34eb1ad8d2458d15d5b7562b9da66431bcf03d89754468341c52c3a13337ecfa6d43b7f42ab1cd22fa79623b99c76d18f8f88d58fcb0bb764ede7e8dee0c717f089c95574948da79b79c545a4651f609f4780b86baea2dbe776084bdd164e1bfe52169a06c87465bf7fe85ca4fcff70e0e1f164e777cff0d9868b57e4ad03ab6dd2e1d8222067f288d4d9fb9fb446ab2b9f7a1dee6ae98ba982f673389a308f770f0a5bd2a6cdb1a931f0b6078bc0cc7e1832a29c081ed9d51d9627301bc92752f8a68e4cbb54baf9316d28c9b582ebc9772b1786b72a23399235c8d4358c1d52177c4c43d16ad31707331d771af6289de5b020d883011573f02721fffca36a02a5b9603d8cdb032145ecac2f054c94fbc96359e1f292bc591465936ae310079ffedefb36250a1a64a57dd51a20c135d2fb697f84373bf68d100e68b488fa3642199976901aef91ca87e1166f56d627f7d73321f5a917790a7404c28b5d7675f4fd53dc55b79d9e627c148e873a7a10d81270288cbe5934000cea200c4299d60f29ca4d5f4460af2ba49a16f0ded04836f8e0ab125ea99d99a6fb686de54a10e9df4724e8eaf6298555f6cf9b2e1c23334bd7407e1b33e2be4aa89ac520de6c3ceb15926902243f2d3bdb3a78ee45ea3db6b46ad90619743bcef4a431e793a30280a06cf499d67e33b282267a8ce31c1319ab10a6783a6f299cafc6973a61a816c8c0b34017c965bdd904607486d50637bfeab87ebb8a495905ebb63b12a6095ecb9e6a286f6c1f4db0525b890ea9b672ff6d280e73b51ca76d4e9a32234382991ea746b11aa822d279707b4c116a10c9c40b4ea357dba1e9f69b6a60113b7a5ef886ab6dfa9ea12f2bfe078f70ca663c7e3cd17278c69be2d6a6e53ea8218f51169e1147af4660c824fc714dd72af99c9beacce18f60d2b648875542d2b32bf33ab5a1cfe1148814e25bb15462d400f0e5709b22ca3d36327d972e05f70620f015fdcaf78fbf8be2b4a5456e21e7d410874578f160018842eae9d5c47dcfc34d818eda4fd65d69a0f31bc13cc801f906ee7eae5c8b438c1708898380bffe466d11384a9b6b92d668e3e100b3660ef5083a3a5291888a5814e14f7dabcc3a9dcf099648c4c1c7eb14e1d25f5d9a0ef2ecc4cbeeaee451867403905f80f4b725994ae51bc9fb4f6d265bc8b7c08af0a83fbe580fdb1ee521f21260922f674543ff00d4daebe53bf1398ca743b37a10124161db61dfafa33205493f82de3ef75c2ddc905721c8323529e94418b7f647f7494486180506244b5d29b568e920d20bf9d368d8d92eee65740a9d1c26397fffe408e97a1ab9b14c01aa57ca3775cfe79eaa853cd4d15bce0261a540a606549ead1c8dba3312e3ac8d4037435d3e266f05d5efdafae913f4b7bc35cb83bb9871fe608d43faa0b4ff687474461feadc1840092fe4908630ab25fa71270c5c9ce87144346c7c8a89bd5dbb088e6b6fe8682c852dd5429fa3ae27a222e75a597c2b054daa424179df2b6bdd52d4459cd42de5408ccad90106c4b481160adff7f8a7233eb8f452bce4252a94d50e316e928b5bd1c6e97d9b9930b185d41471609812866a33a68c6c7fde4e943f57fe169e3f0ab0b5484ba4540faa3ca6b9a203701ff75c4d03563237a31d6b68fd5aabec238f14451048edbba935ef59eccaf07954b57fd9948c08d83b81ab65f0c436de83471b05f90a6ecc4791a32c7473593cd29e5f9d83b67bfe3e6ed6358e8372720b1ef5950c0486e897e4e1ed231011010c349494ed79b9916b4f129d406d9019a8fb8a56fe68431beead29f76e9eaa4eed596429a6d6f837b9bf667602a99e3e9fb40ba8001bb741e800519869c0e9896274eb36eb4c45871bc346e3c067bcd31afcd710071f5ec7f0022f50c17a6a4424316d119641e27740c528d61caa35a155c5da2aa1a45b937136cc966f642d7b383316c523e2745dd91e1dc8604741306a83dd74f16e3529600c405e32c2772885f689b96b171bd25a32dea86247ac83f6facaf4ac37b38357bd78c447e5bd7425615dca5c4d53ee6479d94d68b0cd22fdc3f7a79135a482ec6a15bb91796a3e9cb7ef5b7ee278dedf44c4de8fa1e5d2dc0aa3d81ce918a744300a7016f4569a811601ff927723ff23a1ce53c067d9dee472dd307fb5928a9ba09c151b98b07f54185683f02806d8c5ae7d9c2b8fbfd30ba94a8b05dac4a1a8fba053b3d655554e036af70ee107cf4c6edf05f54238426f9fdc5a336140df1342b49bebd4487f321db5786c55df8f5969e2ffa86db34b999070f7b67d5f9ab871afc0a640d7529da6662f6085c72af5dc2c50d6c01f6083c67f3fe0b874c6d86ac775dad1ef8aaa0ba7ee4b3eb3d9f461c9e08e7d55e8b38a8779961ce2fc3f72061bbe748bc47becc43d8a5395b8df032e04f28f0de906b2ef2ef79ef7574415e3cea3c5bb8816fb5bfad69a4fab001d00523fb57ed32469d7fd001c5917b4072f185998b9911ac03042eb00b9f1c3d041230ca3f8499fa1f46c2afac638ebd3ad18495a2d56f047ae31b68d7de26ddf4657c3f60975b2e8bde3e584e2aa1b67b7691214350ecd1c2e499073577a08484b1e8a23a8388d1dbbe70ffb439c91483822044c5f731195f213fc0fb0f0bbeab560140fc1bc91393759552e89bc619c3452c87c12f1aa20de17a9ab7130faba45e49dccf8182e8a25d0ba9ae5e356f0f95814b08f4cc4322580d776b3d427592aca1b510d7dc4b90480f2a71acaedfc12504b0973ce6b4bf9ee76e65f7c14008d478ec7ceb0cbdc812c8320879b81b104744091124f32b221b2e6caa2c8b2fae4e136a034ed2662e63d12142569cc6dbfdabfc23469fefb7c0bd3139e5b6ff1e5f43c0b8ee4dc500f9bb868b04f17d5a5a4c297404920388865cb46c458eea997aeb628942c8ce5340176d7eb81c0b3b186db7c26a41cf512f96155d4263ef856e4d7b5438af09583c2b7fbcec1208b655fae05d5cdcffe0322a25abd546c051f27ae2b4d543abb9c644d6fe8f07d20e48b00a707a7f8b28957381a532156a1e8e83440c5b2ed2f8275dfd06737da14d0bd2ee6f09036a6da4b458a731c6afccd8b0f4579edd4f192f224a5a443b7973e3c0b819bd03d6c7f13455efab4ab0e4cc9db55fd0a8e0337ec09c04981b8654170d9a2db6fbc24d3bb15cd97ad3a49d49dd7f1b9436215d3810f35a4d3661d2b88d55e94355a35ea31ea773606655668ae3af200af6c53ed69a26d10cbe6cdc702dffa0d77f1dc94574aac43d6f5b6613938835f25196d663509c5be09a8e09af20806c7bf8d99ee35e4beaa6d2886ed62443b1faa857cb52eedafbfa432f20dfe1a09750f5be42813002001216d73eb64b0f2f84dfbf3e572fa3e1a04b1acdf020c6f05527eefc730e86f64525be6d1ee59ac98edd07c3c7cab9a6c96a3a9229ec767b15c5f71ca5d0db02b52e025c40a43ff0f985d196209b8cb70655ba4adbabd2922e91d2554ec82625d3573893f45a509f5a2a93aef471b78b5a7c1e66c8cdb687a8bdb55a416df7af8ac01a5f652f47f63f1e5e0891d04c3f86248c9371f724ee38d10c2ec0ee21df7228f10b878860f9b16c5b7d92bb02397c2f747e809874ebfb43698f79d23342ae0ef8010f33614e2c3b643b22e8a76587cdeaa394953ad6be1fb2702cc63469f2046d08cbd04d0b371f3e20cb455fc52e786940cad51039bc574cf4c5542d03205ee1b24099d94159a01f20d79a9074f4676ca4dae66161b7062967a62bd653169bc2f34cd318c42885b4a0949f975b7b5b096ce6d2ce629e5ad306e30a68c0744165c3fdd175c3cc533a4b823a36e3576adea0524f0e263cb203c1639a8b19a8e1ba52aeeef04555477226baedb686d7845c19d3aab41d22b0bb530303d7ea8d68888fecb94c32e5a08a102c83ffe7a47d0f861c0823fd3c0d402db5f3e99e9420c4203bc3306a70149575ac5e3dd56d07cf14e940cc207aaaa35f0775d9d36882e77756d1391f1cf6da9288feb042dd1f3fe9d192008eba7f4c54c74ee9a7b8d06b2cc7117d09df4f9fd5db1267206e993653f7813bf84135a0bef7aafa943aae8c1953ab058ff8a33410183a01df686373b8a2c0f958015feb4c80856e0ea1207284d26f9fa923be65d51d7eb8ced546c7f244e2645fee36f1ffda0fd759b09fdd1043845589ae891eaf89affb2e949fb3d07558d1461788fb714d374afe1bc67fc5b051ba65fa0424b36793e5ca93ad79fa239eaad5d5c2c972d8452276ecaffcece71eaf7de6dcdeac40ac93440775ff0b638b6d643a979de41a692a08376ad445be471d5d52bfa139bc604ccc026f6e917ca56a5d48007e6e5a084766d1677ad48fd5199a97c3c7b6ee89ea47e67ad2dc54a80dc09de5f88f3e07f250d9941735cde76f26d721d759368505ea4ae8a66fd5c05ee920db99e7532abf5375e8f74685a4087bba8124c67ce3e0065948fec67166527bb746f81620c87ac39eaae7226a08ef1d704ea04f4909e0b85617ae9c9abd3d018655dadffde73b3e137b292092be79fdcc5c58af05252551f5ca86e33a77d469745c8198aaa97439b714e5cfd6f55c663d0a425ea6da0ae13e98332666c385579c8db11cd06ad2145440d03c729c86a138ff911a69fe4e3f175543c43791b71dc6146284f44f5de74e7a574e4c54d2c22039cf004d56d39a889d1e68bed88d58ff53bb0cced96649cc99cc8873859f95a09b082be9328ca3a321de7b953ca29f7511e67ad8a13582f7afc7c76bee0b7acd5ef1cf0a17d75275ab50db3e5f95d3001f651fb6b0a0ebb4ecb53643a2ad1ab0e652eae97f98aa4c6d8a83fa504dc69349af73b8827f963c4bf4e198e785d2b4778fd3c8ee9d7b725f315f536a584eee5ee17a940fd64da1ca907c61d9e4665b53f03c4edae298d5317614a6da2388db78712c01258c479e51675a66775cc009e5801fa5412e16a4a20b93a944af92aaf5266459c536116eec56b6bd5b8dbab8e38c4bf342057d7474f565d945eeacb76a4468ac7406398cbc2d48b001b2d507ad3a6b14e4045394c721c8a1a755e1d58239c6afa02314bdf22f4231320b3bb3b2b77c152222d0d018589bdac291ac6c09b83deb3ed8e44c21f2bf5ad504126e7e19331c24ae241e575df3c63f68599b9bb2f47079a6d259f82bc6a2f7b371422cd2ce140cf51ffaf7611dc56c15950f3aeef842218c7472a47fe8b00bd46be3ad2738293f611b7987937b10f9648b102421007fc296ecc1c77525b1acdddaaffdfaac22c0ef85960a3d078592cdde1db6a8c7a9002210ea058d8c117649126fbf4c722d37ca44b15c3db7745f44d617a429b6fd7ad4f4b3d0e941e926c8b937a0fbd454c063878c00de19ef44181c42cb50dd431acc9ad9218a9372716e28ac58dfd96050a69a55ab7e62d1a702fb9634131378c3bfbd07e8afc3d4505c6c90535ea1e75bcd91f816e19bdd651c64fba449d4d06bd62a4eb89963ecef16b51335177e49cfdfd977b949aaf2c2e250c7dfbf31e3acf047e70010c699fa76dc13eec51ab01457b9dcf7c275f7f7ec9f62e8b6277361bb9cd7a071478d20f9f4a3e80db9e500599479389d810c68a3d77ce215822fa5d5de21f76bf8594d7b0861ab393611ce3c4c1390bf090c0e40b7129be70d55e0232236f4c9ee888b917e3a1eca56f96cf5ac32030d1e8d79905e1744c27de877c18f8d9c6db396dac522b2a468a603adda9444738ee55ba42638545257eb421dce3db50dbcaa9dd93c1fb5237c325eb1a805c8541202ae7fc90c152698bc7429e0bbe10fba403133d0a3783c2d5b8193f6264aa2f33d742d5affe996958b9c1761cd0a8565f48328e2c83d53722eb7f5e591e0868a7f8876efb545dba3b8c70049df22c5a6257ddf30001d70db8f4d7579c77a2b38167f04810b747226e2771c258191759a22e86492bdc25400c983ca858c0864ef151a76d7c98d6f2f6cef3af1d93c1285309decca09e7845e561e73160322929baa61c3c5c8ee48fa600dbad26d4c18c20981fe3d23ba5a22e97ade119f048ab70ccfebc6d348e6096db3923a34836453a38f9e91b6e9236298751006f993ecd251e59fc3bc3d8857f6d79eb8418aab1712d63aace9f41d46b558cfed48da935b2cf3aac748df580ffb886e517cf33e3e9f485b722e5434e6aa390051d708c65531c5ad738a4f84d94356f793d9f08dbf51dafed0602d1909938a7b163d0ea62b0350eb9af3a199c591466ddb1a5e66b7e605460961055771e09bfd3f32af3ade777829f37dbe6ce34c8c6b30ec35c239cf4de76d9d83d2e0a797f3f233083f8798aca6b1df97aecc8dd823ced0038a9bda7ebf4478ae85f7f88e737696564c7e076690821ce0c5634bb8c46c05fbd3cfaf60a605c33ac4154a45d1bbe1e587da92df78b65685447a2d6ff96d746e2ffec434b55d2aca9e49357ac346f363de3f5569d1d65f3da8f78b81a9f694c37eb4f4c5a2b4707d311509bcb14157129ebb152702931fe959f386e074c26197590479697d6680f089a1ed6105f20b3d42ccd5a51accc4f49f2413295b925a1b1223dd13f21448c5551378771f43dc24894bf6f7e25b64a5bc413368d6815b2d4a177233b6e7f17dabb925cbe456f7971f44c7e0e99380ec96e9e60c872bb0cedcd10f25f6ac78ef1196464fb36c345d89b9e27350c556219200dadd44a5ebf4426f57bb36d3fe03806927ed0e5e3f184cbda812ad82d1e9b2933bb79da34768091c723ea97256f079038abab005374a14647d927dcd065b5bef254731954962f13be6cc793ee8c3082b6a39a230aafa3edb3f80beca80e7920624c34476af834278fef35dd3d668cd9f50062081cb1cb9f9d435fdbaf7a6c03799e32144a442375cc9055b0715ab450e3dd51e94e138c2dd8a7e984c07413c1b50b68eb0d1334bbd9525bf3e70c430599246256300694f0d1ab9bffa355a1655bafcf510c0b28bd206388a59268db672db13f43d382cb05093103d1314024aae3fd31aa1170ba738728f082c7efecf7a5603e34609de489356631f6f63d4f37518c8c4c0cb49a4d3dc59fc48807839b1f70171b943113141ceb147f42eb92acc60078dcbf84162f306f99298d3593821083bf086ddda1dad345c77477772e1315f420c92395333a4739c90aab9a33ff81c452c09ae6f6e15b8fa301bed00c94837cb3f57c73d5d419d9f0722e5c005008d338733933fdc506aba203090f8f0e3aa1cbc8418c83356dbd37d249c97dd8465e6d0bd398022f37089052eecb86a5e783e0415ad0c66e51ea3cf03c2a5410e21895573663719e74d7dbf87ec8292fba2964b910e0e0ceb52501dbf00d45b999bf75b11b433ce71e8bdb1d90b1ba7564b38a86d4398f53c5ce3eb4d3fd704464a42b1dcea6fb62076ef2865eb0e9445dc2d6d738b70861d222707af032d0281b8941a2b39b8a5dcd028f77ffab7ae2c8141c5fb739fe27bf7fbb76e0b6f71b708029cde085e47af5dcde3a9af23ab7003c0be4547b7e1e27343a2a265916dd23ce263c8943288aa74b9af98504f5bdd815c785321d310c969b8695f6f17983908b800b85f0f9423baa908e4c7c5cbc2db6b678060df0a4cbdda1027581c7636f51015e4f86153bd8506119202f03622a2dd8f57e9194c879f45e041b2cc71c05820354e25f472717a96d136cb5aca1bfdd8801abe04f38bbc02f03cd1c2beb6bac94c481d745c3c48a22ddf708fbd6361666c68492aa8ffe95a5f6bb4db16f0764181b01cd938092e0195c6d0e9f7feaefa2e5bda465bf9bede0ee4886d62caf0fa04450e34df47d2080114cb2bb2cc3a839d3ff1470715d03e584531565c8977c9b9006809283938773033bbf768ed4c409b87b356282734f4bf88aa4a21e42ee75c5575e62880af331aaf4c81c21ac56a7363fae39483e9cff9f3bc189a5bad13e6bd59ae4a9b743fd22648f920872d05019054c86e29b584561f4dad4d096326ff0921482dd9c6d1bb1d5d4be611903bbd1e3f667b3947bbfacb888ec01c5cea575cbcc1da40348697302a52fd4e12b5a79fe3ef757dc8d1b47a31ec5c54f7af2c1dbcb84a35ed775be8e18950a0d3c1c756e4367f2ffa9662749a38c792afbe15654914bef29b734530263c415e12a0d08c2765afd3174ac99c9d2aeaac41305a874f77d0e3bca65d59dbe92df3b7ad8b0c39fe16ab3e5cafdfc9f53937ade2ef3f3ffdb6cc3551ff1599e263cc8fb31766e4c2a8a6915b4d817b22aad346c44ec942029757e19ef6076ab8605963caa43a6c7713a5e027b6782de290e0d56cfbd5712c5e52ba8ca99606bf724518eff29753514635dcae9ac70d273928bdda1ce41822fa282983e4f51f0227ade5b5306e9310d9d6f7c5c4830d3e3f393522931345110445b4e8d12feda1f65852018fd11007d33b0ac7b018bed1d00981b773e2ead11da8d507084c1b7756c485b6ba0d617ff8d5f9590ac5ddeaba3f9c818ee33716e574ce38f03c0070292201034affff4edaea0caa02917cee55d0fd7db56dffb0206385668eaf502500d5ae34fd22171e0d869a9d290bb9fbba47a36af6738ceae70f7f0765beecfac2e6e817bcab34652d29e3a06488ade4a13c44e0aedb6ef44885c09c2cba416567d3cd2d25612fc54538e19ebbb50ef2049cdd68a9266d9909350c4c4f2dc2793fb5f18446bb895458c37e26d1e336bf9c29ce678b25dd7315fb66b9f7cb717d1b51816fd8f005d0ed0c3161e1c67b2f1c0863afab047fdd689c67371ee1db2a22dbc18aaafb2b4f114caf2ef2fb3320b90a69225a4c4b6c110231bafb8a7b36c6f98761d47c36a5af51c61b0a1b53ab4a9814bc1e9c25b7a9fdaa8e1e1b56d00b643fe7d547b9a9f535d0e799162592204bfe1487ead7985cb379e40a9498314928f19949c4085f3122a5ba1f5bc7aa893139932eaba6cf5a1342342cff9fbab2ba496d509d9936389faffe80b9b1ebb05c18a151409c4bdf9165d60b134eaf63cf0db94821e3df25a3c087e835bf55f0160190e2186415502e06d6e619dfce0c4a22755a6ef6575c5039331b856fd8f7133c8044b0ea571d747550956fc0fdefb59b5047bf09b197978f3b5585e990fd015548d178899be224c563e3e3f5dee70c29b61b1ee931ca7111f64f0b85024944dff8b113ecb6bd393211f8bc6a4bd7014112b8c46e9619a695771137b72bee75b1e70e05f7c27d68e01d260bae41057af29cab53f7a040752e47645b27b9f66880a674371b032c45b08f994d04a74d73ee338ca8e6070660c1e63d37332a18621467b7007def4246dd9228ad8496c57f98b73653cf0a61a7d2749ec743e6580ec09bc690945085312135f9ea83501f356410d6edcfc64a19444291fe8b7a88b290cd2b68685e00143b9a2089d581f7e3d0d98c5a73d57dc24d69d63fd6a4aacb0db718e6721641b1e790ec5672e48961e857f3fdf808b10ec0225f5c39f5b85bd6ec1a85f70e9c669e3936c98f651431d67b04ab3239ebed45687aca58f260268c233a0236fde2b49acd6a57fc0a87abf6bf017018b3706068cbb1c114ec72cf19fcdb978ff9f9d9045bc4148604f0ac884745065fdc51f0d380896a08553884caff99d26696b92a79cbd857cf3a810a591c009915be9ad147567419f47edf92d0050fc3e5b947dbca4bc189ba45c7a5bf10759b606a6bbdab146d26bcce3a6323d873828fcb47bd268c594edad4d560d07782ea5f51bed7ea87ba8412a87e38e0ed4c35537ee994b894c93d30b3426ffcea2b1ae0922e27b107084f16024c15c862d2211fb99a285dfd5ca2ac144911c154d5d6fbf4bbf73c52c216f2d6c34d1a589d45572992d4728081048e5e5736f181d1139db08beea972e89b9126019be6c5c84cd8dd9268bce98c53dcf644cea59300e2ecdbda69320f440f455198fe023e127d48cc6aa77a6ce8f6bc03f1ae05f1291cafcd6428749c1c50ea206f6afc66cf9b6813b04bd67d9b0fd378841ac2b68aeca7cddd76d6216838e872fc9bf01932583b8329ae854dc7cf8801647c7780dafd5c8ab46b06ab0150ade350086f9ab026f81a7cf9fce4e6147b891ab49d3721b178590e1063a5b7dce12dc5ebfcc13152f4cd21736392d9cbf4746932f1f4c0bf494e9156471da77fcb352903468d14b11843f67723811912ca37a233830455ebd180b4dfe0344251a1de128fe7c962f6a620918bb3a30b133dfd2816a0e83a0d406acf9bdfd4844d30618130647875789e3e12a49cad3ff6edd7da17328b14543f9c5436358475f61a4fffa07b5ed295cc962b11908b1d40c39435fcdb9f6655defc498f33ecf43897d7f8dea900609ff15274c41a77e1a2b28c5a0c97c27e09e048c86ba816ea6de726402be058a8b662d96894f57b9d54c5391baeb2b647588208ba732f63e07b03f737dfccfbb60a4cd9419acda41b4f2d1265414d83d6ffcf1132235022e4d94ad4119ace4fdd6185d0570f14466ff365b8612db9b229b0e0afff6efc686ec60c1a7dd351a68b1fca61288a6b5a3626c88d7ad4dbc0de9be8adeaaefde0c73ffca45d8caa55c1f77ef198f41a93fad3327ba12d08c7b32d01218c28559fd07b03fc5ef2b6aaf6dfb94d165e4f709e4fcafe72fab3e1836b39ac04e33a08bbdb0ced8fea8982c0bc0e7e3e4d18d887d0aa868aa9953fe14f5e0215840b4409b3a1767ea3cbbb362de3fef8479a2fb37e2fd54b6ac12bd254c7e8b0506efe55fab6d89317158cfc41dfdc477301c3e8e73839617718145e2bc80c164ecfa3b801fd9151e5d20034fdb8f547e1935bb4dfc2fd6a415f580aa48d8744014a3f62674f4d4332307333139a28ad1cb8ec76f773fc4709f05abd9691b78057b10d2d9cddacb113ed1b7641b057ed9d691eb607bd9b0ec97f8b2440308f96d7d5afa3ee70cbc6c452dce844e8d9708925aa4a35d8d470b3264c25d04d50f4ea72034be5dab1c11c96e55fd6dccf6f56c92b49d2dd4b25bcf6552231f79a98c4ebead0e1b636d75ae6e8e558458c8f7f19170793ff50eeb14dd5b30e3539cb2dcca266d6181df6dbe07f4b36fc047def66cd26cee6602f8c572bf68cc9f9d60fcfa209702b9e9d05878f966742254626655efb6d66e43e4e2cb4864244d6e93be01407ca121f2df7acd752627632eb193eef7a0efde4a1cf881066ff862840861f9781f3f5d161bb2d4e13fd1d5316465d76a1d346801de6fd4472ba7d13e7ec7ce4cd128451bffde946c57b415af49acbaf80df5b9b1e6d8a4804424e26e43a8b7cadb00f115ed9f61842889b4014dd05ac55677e5a2b469e69f20476adb1192018dbceb3d91e4a562b202d262ec0f6e95aa19e79cbcf22ba26b9620f9cd0bc8b387e9e5e7829510864495d13ce842225c27004db88763e348e406834ee06942c88913e9a2b27691a62665ca633f17739bfa252266bb482412433f2d0e40effc38ace56ab2e9cf0477ec2af318dec013c128b6ffe7bcb80eefcdf9a4c3fcdc680e47de4755a846bd91d73b4820d257418df8244ea3acc28f90da956670d741d4c03c9e07ae4de4a30babef378eae7f851811d27f50ce2c6a706b44d312368d8cc20ac3a8b932a3f21c46b0a8a1a523370dcbf3485cb1805c23070e3fdff6841f718bc9497fd8c4f0940b4c76ee9b686eff7ee0a2545842e4e882835f1b493108e11bf68cd30334dfa314712a064ae1ecb2068ad5dc5a7ff13e47bacb8a26bcd55a36e6d44ad22b38c5a01f01ade8072cdf718f64efa78b94adbebd5c99d8cd89ee3ff34ff443b7c1a765c6379973fca04276816a10b725b4c739eb1b5ba073af8c184d0ba4cd56a054466396ed58e786594de807690194c9e80894c2f0f7854f896d397faba7acf1c14abc9e49f927f56ecd487dd757e4044c9cdaf3a47393dcb14ab3c756ac3f56b72dce88a834fc71e66864037cd01dffdffb58df5ee9479f8174e43b97fdd91dd74fda01cf91c09abb35631dad76aa7e5fe4c786f94dd4e7826bc6f502914a2420871abd6a487270c845ccede98b563b7564f9f79bcba3c6426fbe34894a197b7df633c8638f8a994289d9702436892078649de997be262a3143d825a8ce164961b48e6ef68dcd2993fa27b3fed389467dd0b836d719c0f74bbd2a5b312b2a6cf51df2c80db2c1a0251382bfa43339b1b3456b96df8d8cd1867d733bbb318b16ddc9d6dc5037b893f90a9926234404517bd3d52026f214feeea4c76b9cfe8ee5c696686120a398ad5f1d3a11fedc1215741134f55279cc6d0062479a11829d85a405e0a56cff3a936f3f4a964258e6c1b3e953f8c8815810b16573a717efebf1a43879fc8de370977676f2c414d903bb775306d35337ee0037e7abe19c65a4e7ad85104470ef814998f062c36d8c16df11919b23747d4f9cbc473511958fe3233048ad675871b97b4c684d0d641d0a923ddabb0ec3f5c6233283cb5224a0383f80d5c2af282cd4cb66a4682ea89a34fa43ecc6204de0f3a53debb9a1d5ed7fff969a3bf75a963a761130881ca49b812c4c53e475c308e5c17ab4c0003bb11f628de250be557d991d226bd8692ac3a003723493683a5bb00ad15b0e1e120ed391d13afe7b6811832d5c7e0fe1fadd3d38198a50c6897c4d5561dee2defef5a5c1bdf35a34457528e2e5a999c9762b45279748dfe6a396a530851ef98ab3bc3bd5fe07e51d28fe0012921f77fc2215051b1a1dac900496b381bade0987dcb096c9e8aa8f2afc5cfe7fcb61c6a1e1b91f93aed88ed41fed9e9e9e46f46b0f14ea80dd92b10d9f21efbf48d8d248328ad3b39c94cc4407014c39d5e3ce03e0aaa0cf950066377bee5727c29cb9125e65c47dbae1e4facc0ebd4cf1ab98ceb7511d2b49dbdbcd76e9c833bc270a150d9a092dd9db540d305dfdb95ea7ff8f8c37b645b2a193dfbd1dfb25721c15960ee6d2cf2a919ed876ecd3dc249a955a32a810b4961158935afe60c5873b00f087f269e11abfe44d86eeafa6d3d5bc4befcd1ab4d9092a69c62b262592e01f2b9b16ac8a9707f8b1eb7e7219e2b02bf7456103828e48daa03899110890e9ed442ed9843aaa26787d5cbe956dbd09829ef11f405d79bc54730522cd9ca3879d70134e579b5335dbcb4f425a2f3724fa5a5d8d03b97526903734b2aac90d12955196529082e3731e64bfc7b6b4695c1f23201cde9185aa248d8505fed16ed8f10f508f3dcaf349c8effba2059c078f55f7b1c25baf9071ed6433b9399fbddf7c66bd2ad1844857ad040332e84cc61b8daf4e1b096e6a7199038bfc2d9c54fb4dc9422d68807e29099c56f01026b57ca8666707c8335295e9956104b5c057b659eab6fed9125edbc619c935a3744d0e27abaee2878f673e9e5ef9031e3bbfbfcf2b08c272e58efd6f67fee8aa91f7213ccba1d69431a3f950363cdadf09093e453694165543921c77f1c497099d3d0c02a7d9d7647fb2161399943e29fdbe09ad6cef670299bd53299fe0a88fc6832d9ed447264fb6761ea000004fed48d7eac7e4ddd9eab14367d4b2e2e42947de0569dd44ad2a6db02287c4976dc8d8d7feaaa81c9156612d655b1448d503dc7264601ff542aaf3dd5580ef1489d59c53d5eae9094a809be55929638d52290eaac3d55dbeaf4de8942d3076cc6bd9912052ed16ebb5b3ecaf03aafeb80bff1301e347a01b1ce2ac81dd025c6efa0e16e456a43f9bb976f8379673e97e74f492a371c675ad4a737440e853375c998328062b15e79e9e82ac1f0cf8fca9509f349cccf45b8f8343e0b3c6370a7e99cec71f1c38861fe0b520844ee45fb03e816768bb00027412c89dfb2f694d6ac62f92f1a2af49d1a5acded9bc10503daa18a5a72ce48ce2e1885aebf06b014b488026c3aeae6c17ee92e48c8f97ec0e4e6083ee48c4121f71abd14cf1e4d93fe9189695549ee2c2da08f1c2e139ad9a8ca8c0c9e1dae740299ee6fa2f077b5684b51bf569d502884a1708e64fe68b896225bf56489a5f36e6cedace2f175653f82916b96d42e7a621b9b7ea7827cb6aa8d590d4b111adeb41a8fa261449a27c4708be8aec122c8b22b4dc7e6c698adc12e29e57a344b02e7f3bab12e9578517eba151f5d5cfd277bfbe717d8ade6246709b504b4dfd8387ccbaa9c6443c5ebb410c9f7c28fbda1bc37fd78ed4728b46a9625415cb09f2278ff854ec6ebce3d2bc8e2127608ad0fe785003ebd936149a437b4d57340baea7f8104dcc8e232d60bf58c1e6a2ac51588e61a0e13a9f464290a2e48bab7a52edd07d1777e710cd5e3006b9f71bf65586551c37e6217815136b839ecf7654be1de98e8ea25a1ce1688d1f8ac8288f7b612df88327e6600d1ce1ed9510d1d23c5cf03800995b194ee94bf0583303ef262c642d2ea616bde9a6ae8609caf631585eda49590274369fe9163c9f44d9a925ddc24905239e2094e1acdeb61e7ceb2b48cea04b5c97caf488b1f43691e4dacd830b4a62b75f7a431cf96531c749e74571c47e91ffd8d8d8ce99a0aa4db848b26c369593de1f2edb74b7d0978adaa8e231001a77d0172a2ebcb3762e6307cccfef7a641f46650a019cf359b779e61fef4fe728a68ca0136b28f6c345a18e5dae99a8045c77a34c778360a4910ebd06c8b302490597fe32db9e23eaae4159a24de41bd861d926d64e9e2d8853ec8295967c97ca17e8bac4112cbc3f601150f710d51f5a468012bf092e9499af0c1bff2eeb717ebe7ef7e3766d3b7090b0921eb0b7337c3325705ce909b8726b3d15bbd5f4b8b9f7a7e10c338032d8fedc3c5dc972d99e17877cbc826421eb91542a3843d61c0bc151bfb866d9e8ee9a720568dd7406e79023835a15ec69668f71d058e4e36950d07ddf208f36c3dc2f02bd801b8695cf5d9c8334f98b21c92f46db3368a0b638769d58998f371addfb5979119d9563709f1e087316aef94f67b60357378c4a0aebabdfb6e3606590debbacf8e7b8855fbb68e6915d53072effa7b3c8296ac0995e9d1e61394b1279e9eb956edd6d7559233dd4e27d3cf50525726bde19d96471f563318506ee3148b6f95087df200601cc3eecc167d2e5d1551696b0ff856468fa3423a8f195d3343625e54401842aec336a0d44bd5507f35a3038b57d0b099e1c45b0f318d4abc099b109d6bc27717c221619983dd9fbe5676da90fa67cfcc382e533f1fa5fc88b4b24d05ea66434b3fbcdf7895ce424e0df483ad5fd3d435b95dce8eefa63ebcdc216fd5fd85d89d4923cea0ac1c92fad3f21c21ff0b29b58414c2567eb8798564608788525dd31f33da546e1582f83041dad09341d145de6d84ef4b2208b7a3b156c6d66a875eed3e275cc9b17bd6bca2908de16ed353d7c606503a90d608fe0325d99be681a9ac996780222262b292e1dc6e298901398e8cbff486422ec5505acdbd773a97ddc42252607e690cde57eeafac12dca60e62ac868d991f8cf6174d58e78edc4aa1aa1e5cc04458f919f4144cc5eb96e6db2e66dd1475f20cf9754c47f594aa55a40190eb2f60c2d389950ff24b8b433d0621a7835da2e87cdb35c50c1a0a5f882e9f79847d99326f6542f41772b3a190dd06bf42f1c51e7173d5279b9c39cfe5b523de92e036a291b497f6f7d1b3012574580832cfbfa7852356555c6d8f0ec232da1830d578c4d23572b885df76bb7d741dd7170690d727a2eff7a5d9736ff5404fc2103090376f90c0dc5f2f7b89c50a1814d2655860e809a4c3fed4a335d4682c744ed9d902e0d64b9956d21ec1f6a530d9dd609230d8251490616a58471ac52c1dfe2cea59e13d220360f9cf77bdf9b181eb5d933fd3fbcf14046fd6a6656adb6311bc2e3a91de1c3ea1f0fb4f231afbcff4565a0666ac74913f3856dfb0f2583be770c69ebb71807ff996390899dfc19acb114bc64febe4728263d4f13a795670b7acad6e6dd35b7beeb686606e4f3ca2166a48d1a9b188d38b1cd28505fcfb820392e0f83436a6290e0ae8c7134f4c8c1a8ade0e2f1da5c819a86b82ec9fca0029b2d521ad522987dd4730952ff002646eb89128931f3dc9f50be0b7abc353e4046d523275fb8ab6ec347d094136999601d8dc42aa365ae58867e4cd7fa319cd71fcd2cd85210af1d327df88817275a3e079f4877ac9e4b05eb39f019ef670f4739ce84a21981f596d9ed94afa1a0d2414fc4711479ade36c588437e0ce426033917b1e380141ad4b12af97b0c0acf44afb79a6503afcdad1c5226961fb4ddbacbbdaedf52149e51aaa36c3296b5cdf6cf8550ac1246d9e2ea21bd949090908f4b13c1d4d669b5c79995228ec58a7f0731943fc4b569c998b460d79753ea5d4d63632a2416ce431ac2e0ad0361a519fdd813550a67afbc62abc444b01aa579fd59bd6e914c689895eba5110771efee0d3e47053bb0ebcae4607396f7c34dd46268a1406eee72f11c287b0b6f33f57f2906cc894eea423015efb961df910b59dd2ddf5ddeed974be6496763d70e04a26a83790cc01f9d8c54b8aaa525c058055383a97f6f274ae9663be0670ce4c299969d4b6e87d26da2fb262ab35ba57796b4467169c8662569eeb6844bf07a4268ba0c9ec8b38c433d8c0460fcd92c07bfd48f1234e18563feb83e561c0559b81a3a446dab9dc3f3f33ed0a89279b2dc76bf52ec98846f8f750cf66b635b0da0a4b18633e8a7460a431f27d42c8bbe8066b051b99be02bc878b7d82a1299c61824778982ed50c581ee4cb0d2f8289211eadb3106b6a44cd31339d312c8403fb7f6f3c2d966eda3e94f028e0ad4e5d679e42eb274995d5eef52d987282177b130c886c915015e4a4e061c2cff383a716af40dfcc41166912059e8003d71d19651923e54487cae315d8f052451df1122703618113dd6e1d7cf1c2641b6f6e3cf400459f4f38cd93aef798f048c64b48c91a9cd07dc1e7c8487a9e5474adde5bdfe84f86af8430ca5cf84d0e44a5d96ba403a9e342402a53e54eab58d76dd3e1f6883ece5ea60a7ad0fd1b03ccec68135b1d2dcf1edadc44e937d7f7fad3a2c93886c0032b12059081aed39938dcaee470fd54641ffb55a3f148099927a07cb025b5d0e9448b49b672e1fa96b3dfaa3822775fb99bb5b48e30d72a50c0f2556e010d0a12e743d8131db75f8496048cf40b405b452b714d77ab18d5409164d2c29705085785b44a71a8baefa9648f752e21325480b98ba4dc8c01900cd7880c4dcab1feba75b98efc7d414aa65e8827e4a0f9cea43c0ff49faf9a25f129417eae633fb57bd237f8adf756b34fcd722ec7a7176541a8b539d8f7ba5e1bc192b3772092fb2bb76aed73bf4fb6171e223215634631636beb74980e861c1389f5effe093717dbdeab0836fddc8ad9aef957d0337ec034e61b393a6a057ddf31e6e24cfd6dc5e943feb9b786038d9b9a4aae6fe41a62ddccb5b29b4f2d7132c961610e7f7fd55e61e917fe744655081295293775e265d94ad04ddda10daf5d958b42939cb3b872c30d423c47a0f358648bd96c59f7f3d788862186882faa01173710924b2c6d89f1a823a3b59e309ad0f857bc2b1f72634012d9db29ff99c8db368e394248eb06a10f8b69cfc472f648e7963b9763b784c44258bbd11e7c222d3c6546a901f89ce33ce6bcf1b6ed58bd2c8499bbf3eaeb97207c2c409b4370bb8faaa3396a089943116526a6750f6bd6819ed81c8de8b839049f3ce90d5220984263a094cfc1fc5ad24def28b67dc04e1c1396eb9b8a74326054d0666a16847ebe1b73537a3777cd28531139072da1aced54a8cf620d51ad0a26fd52856f4d825e1f9efdd1ffdfffe8fa9df5d47d92d74139c407bf34db6245934598a4c5608b7fee107eaf85e9f073a756984f22a86f6e66417da56a6a0487e99454e4478d23aa51a5f3a740c3229ba520b447a75ec982611ce378383005a2f33b9720fc4c9d6f5047329a2c04aca866d35c1df889e90aa030f11234ada104eae96b69526b7b8939431c3c43adef265b419089af0d7a8e324da7596959e59c90d23be6d00165f45027f1da9dcad0f1f88dcf65109d89f3b0809776adfc2412bf4609865f779632fa6028ca26aac10dbc46e77b024fb97694226354721cec35fb0803d031de2f5005f89d721f56f96667d323f3252ed78756d0d13c2b63c68a7d95d7224698edb6e378ae869d259ba301b7e1b20b48eca3baf32796a9be6a64f6a844d4b003dfb5f72bc6fe32b60677bac105c691a54b37acc3a842818ef20224cc87dc097ec6d5731b4959aa44f52e98cde767ab5de79f543f8ee2d526ff10f267f55756e837e8712b21f4f5e7ed29891f4e10db51077c9753ffeb3b9d64a4208c6334245984e7802f8ee3582ef9f8dad7aaea3eaab0e9fee4ef4882777c8d92d17357dcf8e453f5f1c1a48c8a4379137a6da3a1a100843507f7eccbd9c2b36096295b10b3b60040b5ac5824c506bd4aecce5626a9a12a4085197189244ef4ada79f1ab951e11f81ec8b9dd0886f9aa0ce26d36c0160ecf6ec86b86b307d245b6502877441dcd845f18ce3b0aacf876113fd26ab145114c88f52bfea8fd975a5f2be1e759b5612996a09d6159eef5f89fd0c64d5ff21a2909d29490438cc9c667f8bc999d0b6d630c77fca1b233898e7e6e1f7fc939f40eb7045ef8fe33b6d86832062a4984766eafd22aaaa339b22fdd279c7f67d98692b2167ecfbbdafbb84bc23ce0e97aa2819e928ae43567f67b96dd87cd23ac45668e5b5107554ac7b351d92d3fee003aa35065f2681454d181c8c6e190550b90f37b30a8223375efed607dd4e1c812c29da4a7a2026d955c68fe686f9000b0a70c18bacc1354ecc1ce944d4b6dec61a705bec48758440b9072e4e2753be4e9438ca010fa71f5b1516936dceb3fcef62af0d7f0f1e8c8227eaf5fb0598178f9c54a667207480d4f470c17fc4f0c830057bf90f675c88236ba2c8a6759e0b125813553a1b235d0a2ee0cc1d0e62402508ee7c0edefb432c85fe73d588c525ca84dc9b582c67de43f3601bc61970fce5c7628916f28ee62fbbf1fb13fad4f175d9b91a0f4fa083dbb5b7906ebbcd52d7e3bc307dab3db630a4f21092026ee5374171707cbf63778835703e99d8a74ad56bebdbdb793feeaa4acb065acae8a3de6f86c4ad3020b1f975810a4cebfb6802b15c25c581ab79e91a7e3522e67bdb5676c9c96d5ac9a606bc7ae0c2b778a9987ca2060a3df5d928f896186d381fc3eca399df7d2b1f6f56aba54869786083cf116d8a632dc563176966b1b187b8cd07d2bf689a102664b95f1097458143cbe0b9b402978ef5b4da4528c09beca512d29fd06371887e8e514a06562c091dc99cb1969497bedb4dcd3fc94c78185058fa7b166fd5823420d346c94cc72c09a3b7be7bdcfaba36c850831e13937fe42aa16d401205ee7e6cfc3d12a450db9c8344785add56a02f3c1721055a2f43f27a5f22cefc6f297166cb24f15e4ff6b0104a7032e267e81a2efa83ef5d39de3e8f958a03e134ffbea866e5434f60aec3dad7170fe7f1363c2653f2981b917b51e7a41a95fc5f78344a0efe26737323dd8d13c72c20ae2cb997b63de0c31c5b04d15d719f4325ac8624983446255e901ce9a190d1ec40cdfc6ee79592626b8496eec09da52c054a425aedef9697fb5e560538bef96ef27a65f817b0ac94c01f903fc1c6e89eae6a240466b47bf7cb8c81fa88e9583074d197a7801ccc8989b42e837de654cf285af4793d2a5b772e54500b235d303eb3274880d6795c70c772b7739a2f7fee920fa95612c3c5f85c2d95971d850b11daa590575d5009b8099758ddcc3ce2c7c9d1d41c98452915cb5511c511df57745dc2f4c20247e6b705a58836cee6a8d91ea7ab08d2b29114ab7f3e0b4d9ac27e89397bb660ffc36d4d90a6ce3a6ce7c31e452f35aa125a797d2cd85ecc72ad00073e19907ef5461ea38537528d6e4ab13e69867f0aeebf45e950a7c1be0d332b84127dfc6774e253ad7784eb9fd6abfad07d79262b50fbeeb9e96844ef2d1a48d9ea9738eee62f74b57b7f845064ff48d83d6502290d36004ea7f8e69e08dab3b832c3d9e28cbd6f5419dece2471b366fae07c81e7b34994cd3a55bde96c5745580ef849c0f995f90aeaf674e65c28f6b4a68e8d2264ff72bd295e6570288fc66d56fa1ac152bd31a5fc32e0ca29207d85fe59d67f59e78c858c53936b132e898a5465c99987b0d1b3648a324de368a6e2b0a2b985976c631fa88c9072761d783049369ecdc6f73ba97c9f98d9921f482e2b0026e880d9606756bf824dd68ac1f39295994579e39978bc5b39996d4967735db3ccf2ab311fd5404528614d125a5c7c21b9cad7237ee8622d28498a47a0e3898fbc257b4a38dcb8b3aa6d7a0c45aa0a70a0ce6cbc6c60732718117e8a489229561b1be16a39c74c4cf554897dab5d4ece5f2f4c8bc1bcbad8bbd0edf49d7c7288344f2aae27bd5650d8e005dc9f6531a169c6a8f04fbc93939565c95a73eec46d39881ef6ab123d7ef0c5a05eca7ad77106dd45560f73918b56bdf12910fd87b67fb5f98368b0ea7e3fda30deee89f1d2b41b5e6d0de2c31b28436c6f09c98ff1c5d523ea4404075d1a2b13d20f8ca81245a5213c9a306925325201f6c52b56e6f996c734a60eab47b6e395c77fd037b834b010396affde1913f985aea3c1f1046fa186c49d59a49af6edb1f0d93113a511674d4e88dae74e4bd43458ed50cef381421bd6838e96a75210b18990b2efaa2868601660a8d424bc61596585dfd8a528b51e847f128f80b67ad23eef4f4bb8da1e7225e23c64a31af3802135f8b77b6a68cdb853207351c9c27ff419b814a2e51820b6d83f421b6d24fe0dbbb4a8c405ee205f0d9412dd575f138ead4f96e7d7cdb9bd958cefe97b35eff11f33cae514a434d696b41ae9e399138a79098f4a81b3a986543f212fc04356d46475b6a39daa246cc653d9517f5671b9947421a3c36c01acfc9852dbb5c88daac00926ac8d34785878f95856b21ffbb2494d6047226ad083d24d503b15c7ec9d283d2aa5099366e0254a86346bcad4a5c9e8d509f3e789a30e67cafc3453e9dc5b3b9a67ae833f6152cbf0c0176ec320159c38d00862524e0bcf410196777da1e15dbf974c24266aeb70ee4e99e266e5e146c27dabdcb300db0b957e231207e0ebb7a45df75749aed739955a12f08e8d7771b0eccf68f592869705ed13dad87a5279b89343fd1c8990a237c3df91b116d8b5888cf08c82f090692665ce9c2157a3848dce42758de5b70fa741aff5f20510bbbbaff6404f817cfe598f432f89a9e5f3477b3c093396628e9187ff9eba72d75b9ac173baae4501dbe6a9bd647a68452b7fe9fc8e1cd435c47e15a7ffd76148ea3d3e3cb17e335d2382aa375992f3884babb9e58e342677f02203c8886b3e3cb6ce9832e146275897700ff872e811fa8df5c67135681ec9d3452df9a276f118fbeffb55f52f7c15e2e8f922702a6a48ce7679c8e5dc46391c0d507ff0123d827c8b9b2abddc47e8c99de467aa1771d6fe7b2a3cf6db53a93210eb43347ccde64ec989f7d295c2a2f5e2ee7d8894c4de0177a4f360d84a6b2a7d28d1109686b025b9a787fe1556cc1abefc988c97b3ad8a8352ec927ccb0a20913925bd56afd3483b83b30e8e7841d5bb24c79b2a5d33bc821fe108a275bc7738a4761e6c39be97428cbb91763bc102575c593d11cad1ac6279a76e262159fe3448aa4325c4d0bf2c4f5630ffff52b2ec3e67a85ec389dfd9f411940611d37e6be0ebfbbdd4ec9738984e4517cd7a94a67a7e7307df0cfe242a1fe8af1927a7dbf6481ad9a07f2a2d9b8d1c3a1a54ee17a67cd3ec7237039c4da3abbd8f7a47dcf264f71e7923b790805126c89eec917175c8982d77423a376d43c4d970d672815f070aad76475774c9b937214becc9290c427348738900b7aabf8af7a463f6d1d2edb89ec521ca8adb25a98dda35a7096d4b5d8b0f9728907ac9b917116e13ea47e3766e9698aabf306aad3b482af2c8eece176d8a5ed946207955ed6965595bc5585040e0f5a25d19c6db5dd39b44b95dacca16ad2662d4d5608cd1caa38c2b324c7f3672b7c7133521e54de8b7fff67d5e38be02da0c7e86012f12c212fb41d15d55d5e7091f1ec24123f5351c999c996faf6deaa5c2dcf3430dc66d49325f365e94883231c30049bf97a693994c6f64ece43ca98ef7ff50f439ea1da6da9b9320c68f4b7be2b4b7390587755126c683ff509667851a2f4431c716f06122efdd05b8b358d8c64a34f22d6a855e1b61f733ea792e4205c36806d286bb9cf7af1ddac0f89cfd54653561edd26398d398deda027984f912870d085cdeb274ad65818b59fa0935135f1774f18fb90053205977f6da7aab8da38ef3ce89c485816e93ad17370089b91b71db76536e0fbb30c9ed4c44a8b10864242e088b04787fe23bcbeff2ed1bc194aecef71b9f673b317697041562561a5b4170752f0a3ba853c9460630405ba97cad186e6d53d0d94c356b5ac3b6c4b907611b6441fd7c5154b509bcca79afb892ae27167df87233edab827b6fba42aeef4af09a5c6032a39753ad11db6d5ee75ba3beb2f2238144923bf90f8641948d1f41855d4b381a6f99ec3c2ec754ee104cac44475417638ab1c4e7ab13da1359c6cc039152de8c83fff081ce5f166bcd8596e764f81d0aee13cfb68dafcbdbb70c6aadbffe89707658f82b7361558b37fa3f84bc8cbe3b4f6398a9b30b1675241cefe3443a6848154722ca13febbbce003ff205225a6174bb46771d6e81c67c728b2fc13c8d0d837623789c6ca34c536bc49d5af7c3e154087c709224bd590afadde46a00709d44e850fe148c52def2f0e1de3df56da68ebb92b69b17f3885b7004060a2cc839bb7d4bfc84b4cb2584f30d0ade3ee2f48227a355ad7b96e25436293d209ea92d6ebbeaab2fabd9d17417f16cbcad4eeec8f238bf513386874ea998ce87e940176f09f19fccadde79e5db2a53f27bffae7fd78dd0307921bca253b5427582260c4617a40c1db7a51561fa0ee3169acabd9a541b798ac6b3798c4fcbf69b39bda74aaf9aeb2139dfa26c32d68c95733bf8043b5db4ade31972ed15c564fd8260e14c6232ead46807917fd4811ee525c17fdeba00f0f5a21510816dda9b3f1dec00b21581c49741f548abcf6f9295a0719ee2f637656eb568671d61492f1182652bc2bf82352d44dc45d73db636db416473c593afe4dcde68316ed9a2d62ccf37a7dd1dda3a2292c7f319a7fd5d992a6374b1048a5be660df0609a3120f6337882086658c18acccf3845497a22075e2639cbb61fa3b78b5467a5369758af5fb177d02c9ec8fc3959ba97096460bea198538a69051c0b94b49f0cfb9d1610c114e7aabe722219e0ada03d507b35e6194521b75401c91a438538c2c0bc5ee7c53b88a37f2c59d03155ed603220eabd51573086a3987c33ecb4a0ab5f1209d4eb314d445dc30f9ac6d337940e13939bbc2f18cc14d106d9e8c5fa9a23b5ccafbb8efe6701c7392853081f67498a2c4fc9f40945bc44a518e822eb9cb48e18b04e660a6a609b1d3648f11d20a1d345d3a91fc362f7693270dfd0b25f7ead311a8d4789ad567f602787727a46654e68ee110d15a22044b0c750a2d911e29f0e19aa887f0c8e46bbbeddc5157680a6170bb8c468e041c5c2cff8e8cbf82c0099686b7b859e82a2856eb43a102b498d6a0e2b9142b8c106c7ac8cccc158208aeebe21349608d4b40146a36e2a2317a55856ce6802c31eeb3277b3f75d5e8105874f28a29ed2d7c74cbcf08774063226426f76427889a0f16043183a105811c0a224042fc3811e8c25f0a34961c29ddaaf791f9c6169b98c20c20fe6f4148bca07a496e7a9f89039df81f9a5ba5598b3616b5678397815746096a482fda96176f92c8c475fc61a0f2daf05c8b4ba3a903d4f52d7651ecd3de3044f5c140c63774a4d87b7f136dbfd42f0b213180b6237fa5d0df7bb5febb137612ed970a6a37bca850fabf45653c9283690e8ec3076c8959bc9bb782031ad5dfab843dc91508210f9a4704a431d5bcf96aaa3633090f713444369393fe83dfe58789d52dd76fa51686182a2244138554aabf846887c034dd1fdeca2300e8466f4afbf6e2457af5e8acb7178ebb5d5ed826719eacf0ec79c355f781847c9d61e3a203c95604393b939861db582874075bfc39157dadc953d3c38d85c7a8dd23d7864839abbfece3fbbf0b59ce3f3425bf35568f1118b3c080274b8f124c0e1a60e4435a491b6efddbc40f3f113e38787f34b3961d0d93e72a7c17bab1f10378c61bfbf421c7012296dce8f66e338e9b82c618765a7d2e6cba672953580f04c3dfbb498e993cec2df15677959ca2f5e06624ad7d69537b66fba27e61dfbdd76f0634b142e0311985398214c9aacf8a6c09c38dd4b3dd16983daafdac836c51e1123b8a682157c1ecf2722c53657e8b7f2cbfd958e51e4bdcff008333c6f0b56861f1181afa710b5b2e6b0895d8425d0c20fac0f20aecdf3c30339a29e27c290f88846f2c04f7f989335e3200fa4e055a69c61b12e0e3cde13f725f79eaf45bac6f50ed660ea2f1d3b37ccbd2b9fa59db640fbda6748a24af23081fbcf7b0374baa390a62eac761499a9931a886a9ef5df70de29878af94ae48c642a93b2d8a0cc0804f9034d9f5d57aa51dd641dcc8caeaa1f74eed8d6b2a52e31cb9a1d5c1e1f8da5d450b4e9557e10c9227cf9833959bf11be9990c9147789e1ee654370c882f1f38970bc9b1c56503ba99902091f6950880d71399374fd2d4a56557a57ad105bac83f3548a06d8951bde13dba29174d68157863055dbf119a83fdf5ed9690fbfbae310b2152350337d7346ed0db0926b70908076473a3944cb6a479c9ed61c9891d2ba2ba175c419278f7e1a2338ce21b1e354cddc925e58ac95070f6e786e93b2877d162480381985bccb09865dc8c7bbd67f686c19c876679957051badf98a8ef5edf8712e588b83ffac280e115fd750b613ebc0f06ba152605ce2bb691279de4569c3f1d898f0c65a7d64756c54212b56fe00873a15eee1381215cf6fdf7d5809af6b883750d24e9c3a642732e20593defa0e1541324bcc40ad13f1285746eb8e48633f79d06db60f7e4bbb88c924cfda723b3cecd320f639ff29c5e9b08f3f6ffbbd75d0eb967ba38abdd167d344df885f73c7f1a17aa4cd0a6e6b9ff10510e5ab6d38c7d8a6ac01acef7638e27f6501851f91de1229736e2ff8c11e615ef844f0398aff3ebeef2cc8901f7a32b301f2e42070d79ecbb9985e391467a7e63e9001725db61d95c37d0d703e0f4f4e33e6f85a19e02ebfc80e858693c4c87c1ef9746cca8387871c81b9537ca49e71cd5d5bcb91c89634572e20ae0e68913ca5f8b44bce9d75ffe4c16c3547aad4e1c6f4fa94972926aa4a098bb34d34a04b28b0480a700f472c080764c573cea0466d53361152732708a68557a3d12731f42336dbc3303188db2382babe2ac3f42ae6dda56245fb3887de4b19d66654471e53794cc327425b7274cbb68dddbf18624f5e6689249b5e7f3118b9ad1082215cc0e663aacc8c5f2d43626f0642bc1875afd148e07277b98acdaa5c072eefcd8dbfda9e0684f0c6570824a9b97e80808ccc59df49fd0d9acf3752efe681dcb25d83a0224cc041b7156649b0bd8825d5598b436f72dca9a5d33f877f3218480bb6c1aa46e78890900dd29ce633ee02f02b62cda14e3a5d691e9cae5d0898bdbe7976dd63b907bc5efd19aa5183cc831b2de922dee4229a6b8067d7428e798c596a145918e27a7c5b9bea19770ba737f99249049cf024d1294e5cc843120bfe8a1a24e602035deae12b1d2429f90221f3be0137e71669dbdd14575997d99f8f2f7c195c7b56d8fde2abfb0b6c5e5a54272b711e6b6be43479d7a66f6cf0567c852ed4f1c5f849ef7f2db1584da6e5221fcf9fd977acab8c7a5f66e374abaa3a582811f2681c0cb44bb35c8b6da067bcb5cac306fcb32653b7f7e7e58d6fb2194f561c07f754a289e5aa796e760c76d0a1dd4a2922da0b5ba7a926952687ba741bce12d5147805273b69fa67f01597da52b4822a4809b0158c2c38567554322695babcd367f2dffbb39c43c2b2a5a5531ece53041c4a33a422414cc9b4d5cbc887f2449a43dec3d8d2437aded8f9d99f2bcf2f2774bd24b308e740cca926335c9dbc23f6460edff8b1abd4d1ac5641541e9f1314df3f34a7d62c07d4cc06b7dd79c8e41315039a5b770f5b73fad0a13a7efc57613696eba7d6adcf61751f0b217ea2828c9a1365c490073b58da23657df900d6792ed8a51c9e150586095052db090303219e9b2265761a606100221917448976ea4481c446b2dea1c9dee355122917ac29aa491674413f8eca4f6454e907552933ce7efe6824e1865eadfb55c5c954a1dece467ba601cedf690b157187cfeea586c3f47bac6f6940c3513b6af1331fb65da718a62c7087f1843108eb002890d546b95f8ca5fc9a5cdcd0ad49059f63cfff1e1bd66a1503a9ab6a393e44c836109262e965aa611299d5b78fd3a045f6bec1b118348e567d850e436350b676b76d76e1106f5009e9cc57e60fe939e5e5c90cffa1cd5cd023571931f29b2837d0664b287a0387c3162aa21dcb4412bbdeea0800ef60b387ca4b8628638279f98c5358516b904b846d1bafd2f2f78c25bef0aaab39a7f6d57f7fe23d5dc248612d9feee45245e4eb5b0929f6641f09daaddd82fbeddd3886a3803e9fd5e02d5e6921c79deead5fbdb76454303f81c89bf6a3fd9b7c17659e3155944433e0199bb603ff100d15af36bcd76750bf52ecabe36e1971150f8825cce3d0141503aba3d618aec3e3daa6913e1a3f612f0a56700cfcf20d25f94bb8632e551e40cb6a520083dc0198308ae3c6fcc01048408eca907132361ebc49aee1c46d4cc05f716f80540d7748beaa4718a8c12eaa9795ee170d3ae226fdf3c3a54fc7034220ce4e3cc93e4b092b3901b5c52e61fb74a43b497bf365d845c542d5ee15b701f071a1031e5ba96edb9962669739d5e7ea12d04edfe0c038d161a526f20da3537cbe6cc32b86e20116e077520b46a3d0c6b28679697df52b4b1727ac19368647c454a2800ad7331d19642d9e22a4b4ae9ced032b764a75fa0479df7e1f5d2a3199ca12e985e96c6deb8be3236d6724c57e01a73757b88d9bbb2db6ce1c075acc1c9cd4c088342bcc128c52f5d5af60eaaee186278a90729cf4df95eca1eb407a581df5df3fe7acc906a0371ed3f6d99d3211874d9fbff389b019fe88123eedfc2b75179bc5f74730459c99b6d872d60827ea9a5b7f210c7f8d8ed6fe869fa447ddc1c09dbdba85482d4e85085e3448f85be395f798177633f39e24fbaed065c44df61bdeb035830cb50a1f657cddd6a7937d0c88f01bad94eebb0d739e814410abcc000d1d3c1375ac394c0115f8e48300d15e082d4a018e7b227a25f63ba0cf2534041ca888e3d8edeaa6b2a073ef6d4ae0beaf135b517d165c1a40c1e86e65fc00bacda987cb8c823c8f4361f6ad2e959054f83b134f7617868f02f9bddbbbaf48ca2a3882571bf3011a9c1c31da1da19f46016d62b6cd7fccff7205604b88b6b7043289e7a1240839c514007cc2461bdca5c3ae80842493ccd7974e81ecac221b1c993a000c8d446d2343a8ec5154dbce45ee5bc4e98fe69d966972b5de46028252370394587a501c64c6c20fc73ffe1cfea72b11024fecac5fe492e3fdf4b6de7294ab395bfc86989cf6edab8c9412119dc42a334d96783c730a94149670745b7d8e5ae982d415c50a4c79475b1c65603ba88b1907c7c5df1223e1c16fc6fce03a6259f62e0b7eed7d8fb67bb2e9a1051f9ba32007152e61659cf927513d43083060bd634568f8172da85391936400d109253305266581409a6b2f7d2f58a8d77e349a44fcf2c00f4ed66d2d5ee3bfca75ce9a88567822701ef1e13c398ad86e20aa7df56ef519506edb126cfdc6e4eeca85d3ad47867479e2686a0d6db3c146520e6edc41459e1cd70094283af4a7ffcf2f6417b47fc101544a699f4f9b8d3cfbbe217903dc22f560996105016115b431732d033a1e071ef0213ad29b6ed6777c15c101780eb66e3a52a3ee0eee9a9acfccaa68ebbdfe13f9a77537c200b09198dd96f52dfb57b8e03c13995090e004ab031bb0fcbab14a692105ddc203aad7c13406caa464b8380faf8f25268ad26802bc94dd2f6e2a4b2001357978e76f21874fb1b7f385f7f4cabe3bfd3f7435cb6a544c4c244105b68587b723ab88a1bb9e477caaa40c4469088a75f67a809089e31a01b338f67f5b5beb168739d6dba8a6328aa134909bb0dee6f30485fa7e4c57b3ae459fc77aa9464a4d9b0f84e6d167c6aa4f7058f0cc9b44817e4077d1e36737b67036b87f20a9f3906774ac47a4cf887bf8e67de03b0702d10f72a95300d722eb398a22e635f8c1a47a65a6cf405d3afd945427f3c7b2b26fd68f26330126cd92f1b6dd948db205e3087b399f02b02513a0f542229a725cf96f615cb08eb0fa46def63c6928fa8b80f33e5385d000dbf40a1f3577643780e86cc548729bf1449c9a6c9690a7ba2846aaff53833f1d7e3920469c55cabe1a5be7ba15a5ca40cc5fec123dac946296c23e68f0046b5a389db46cca595d7bed7a55a6db5eed54de06a5a58e4e17b26109d96a15dc9ea94a77aa0acfac189cd1f29ad82358f51e33a4b44bd4bc1bb76cf8101945f364a1556e79578c2bd2057ba3103129f097093d35f357a8288033e4f6886683567d364c9351bb91f7e47b1542ac1bf287356a0531bd9510f03c0c34a963a6481536287a926b7470c10ba0aa651576f25aac51469a410682222ab11a2eead6eaea1d74c162f504b0a8db8ca02d622f13099465250b9c1942bfc608f604a5a7544606cba8dc367fef05ec89290dcf99fed9c814b5a686c8fbba6534f29e4c0f60207b9806a5e8796c087098e0dbbda2e32b6b4bb97926462933875625f8c4b6887d006036a3d6885b2ea3b2b475d5e085a2e172e463d01c62bb2c968f998668aca1dea9fe5d167dd5d11ab76b0baf7c68a6433bc2feb9ebfc5f0b7da7d22506e5957fe45a4fe9706137f09f32f85b4bb749a6a350d1f5b2b78cc11381c0cb5aab84b902f5f48e3191970f54b089f84188e78d45996092b1641a47c45df2dbaa94f59d4721559468ad195b14a7309846fefcfeb9b42f7c0c99f1a806380c6865130f4e4f19ab7f0cfcc26842ac74be37d92eca3ba6d1893de841075653462b5a54e8b961c400053f411f84ef1f1f8c759f18736fe677487094eb96cc488f3926952e41be8ec5369a2250aa620df52f4389d7db4c047551e3a2fac02e35036c0968e244f537f7d52c0b4b5f0b9249a5805a764f8f6dd23eb2e0563335905563a8da3a65792e35194bf687cdca35f1c30875919add6bf926888d12f028aacc72ea01ec703345b3868879e3f74b0095b276f0a5d240cdc180280f7dd26118f656bcdebb0cdd5d2fe2fa40e0ea4cca47fe352ce2009811e4ca6048cdbe94844541891953836c85a3a18403cf6ba6aae30091a38553ac8c61d7ca2ff16d7d372dc8c2d4a5a1529066bdf58882975ee11fd4acaf71d5c7c9bb65600c2bd460e056b787f6d57f7a812f6b19f863774407179e640b1c0b854e3c30a06cfca19c440b857fa2eb86928314fac14c047df51ec100d4e0d7768bec7cacac9a5f3ccec10d6356e7cbc4de9f85ac24485873d28c7531a86a0287010944fdb1375a77a1e07a3bdc1a0c5cffcad808d66d3c42f3bff5cef51256b0fe7ecbf50c5eee19f031f94653c0610ff245a30230518ef0e030821ddf1e7403ac8723aa60e95ca68d93c38618ab79252f2841dfd40c685cf660ef2df4d9beb1cf97973ae95a8b1b9c37dc4c5f27c65c8ea2c0669255b6832e85589dfd54521dd836c7442a9863a32799bca6951cb2c70acf8b0795527259bca1f3acad0ce90877d3770dac9edc3ce1acb8f29bba4a4b90b79e617c494cccbdc8c6b1971e5529f5fdc8ff78917aa82777b2d2ccd89f798fd11a79c69c208548fa604e0c286488c65c1f1ca2d065e7f9d84385ad5ce489ea7bdeb52cfab914d37df116b8e334a33ccf189f8323428158af6a7f1d6de5272b85163a8ecf0a95380ddb966139c0440e7d99f04baee2f8bee749b77ba8b3cf588a6221bc52a254423e5dcb39d81b93410742a83d75cccbf7a32ffa43151326918f3e0253cc17c390c235fe375ff798a0f866bd67ceae4994be251c97aeae36b9d499d0f2eb96f44b6c339495f171bbc390b2f28d980558a5e9f21141a3f20820099ce2661561dbfdbd8ec0ad9d793e67e66e38211905a4005ad124f2d69a698d14a1a1ec658aba9ef4d5e2effff6b7e2bcf574d15fe4543a9e6d1e707b7764f671378fd770c66ea2d8816d0ad07519f0fb26442e41ba3636f92881381f60ad8f3e3f04b5973f66962f3e52e29ca617103f7acab7d4e9fb8fe0781d5ee254285045119fba51c07ea35440c9cb8051c308d8a609fdd7f25adbeb143b37222aed15c56a2801025cae46fb17366d41538d29405580c8c2fb097281b42db8783e5fe7d110a95f1db41f1d09f260c1fa894d05a1acb8a308b2d339f7e115612cb793d9b691e7aadedf203547666461cbfcc3907f93617686d42157cedef6ae63b2f169aec5d3143af06e1b149bfc279f5288a07d6450c63ab6c1ae61ff4fc13ff7d8268bd74f034df08518a2a91ef0aa2a04cc7e2a9fda1a4eddb8b9923cdfc351d2bb3b8f5bd0076b6d831432edbd8250e43f58e11dde5953a6f70d67ec4d73d4e51f217bd46830ada7e715bcb89b5657cfe2c8bc11522dc005ca2827c31325c3227ed2a747a0bb3b7e87d405804d1daa5c75331e9990bc675a0983e7b4f7e694fce58853dc39ad83d23db764eef2f8fca3d96aed7ececd3cf2b5e083e17c7f754a3ed704e71082b28df0c70c7b0c6397de7b2e9746836701c2d241d980d118338bd9684c52951e24c4c9dfffa208482d44c980d1e8a2ce7627d26d902c5940cb88275c71e2bf0c715b6ef447c8cc7cd6d4ed905884f5d647ebc5dcc5b8e53046d2582ac378c2b59b7bd341ee4148bfc7f92a84acbf1e6d0ef7af1896a6cdcdae8baa47fb3d94b7f4aadd1035cecce3cf66401fcfa1ff89fa2c4cb3394bc8b7020d7a4ff09cceba0293ca9a8fd5e6dcefc3f1c0e40b7b53c9b162c4257c7de801276ae0e7b1f974834eacf9bbe00e2d90d1ff3154bc71ef7e6d0e2683ebe59f6501c6a2928e6d09ba49b609aa88e222f5c3ee54f3dc3034214014c8f708c5e861fcfd7dc974a5d4a65b80a1e43071ba8c819b0c3a28d156a5155e2bb8d24d492517f4eeaa92437687dfdb9e01c1c50330866b8361565cea14be099eabe6f7302f9ea2b5eaa690a582dfd2b4c822ee3e1260bd1d3c2f5e784652fd1a7a4c92e2984413003d3822236f95bcb4ce3c882e985ce826d265ba0532e8415f83e82c50ec19e97ec2da3ae8744574b15c8e4c7b19ba22619d35221833a06e72fbe1046aa57d9659c78ccc182de2a19f1b8b8ad0380f64e71e3f042663e196b1a298083cfc1b8329ca143e407a2173982dc3770dae9b2f4da49f957a51ef2a63a9232a5bc01c3cae1b1a128c77751a066a1528eb2b1a1fac1ec2cc05e68c675bf19ecd777bbc1492e52ac252827eafd6063d1f300d4c3774de4b3f6302ba1d790b605800e92a1c27310de5693e8a495cbfa3f87eac7ecb931550f1988fe9920a443f82eca1cca0faf862b08bdee59b84990c42e257f8259d3421d6caca0a0bc1ae58b4964a0d7976f7d70c20711106e1b13df5a018a0921658221c311ed25ee9486eda9f7a9e7f040095ada5ce688d3e6ea45c09f7ba74aedc7d0f1fc4eab045e4079ff29e0e461220ec36d79498d41d510c40c70a6c927cda02afae360c5ffe3f2a960baa9bbf5276c13940b47fbc2ef9c42473826309d35c02a497e6fef7d17dce8ffb317e65441beffab0642054b9213ec715f3ec56328a0924a5a3d6ee8f310b4d77ed2d13eaa846f8620b3b7f6dbcd680ff83fb47120c7ea15da03d6f7349346a11026f140f8f41e47ff987cc4c852246b05a9b80071dd14fd652b4be289783a06dba2084cf0664c2465fe2abf17eb0455156fa64c7f06b5140a8e974491cd73637563af59071afc469f62ec4b58309d544dea719828a5b48a99ebdcae1d73b25b519811fd2652a576a88468b0754dbfb3bada794209d4fd31f9b0cc0ddd9e6ba777a3831546327817f915953cd3291c94a9899d824821d4dacba8cda02f79ea81baf95721f7627f3c5153773aae1da1244d9bbcf4b7e48a17bde4934d963440f79181cabc9e17b6ce59518e170d288d1bc71dd1e1c7132d97d69234abe6b6247301d28f78f91ba3436a72f172846120b045757ab99da358e7b3e5d900e4978e4c5a748a7b52f13b3c890829718dc16d0c4edbede05b364cbff6b3442741a3d36bd6c60967eb51b4d94122b5662da48af8a85cf67811a00e301725f44ec24f7e3cfa080e4d0139d470e759f5627411916ec49eb03ca9efa4f9f8196e70d8430e2f69cfe00f7853db748e3d56a7e8f53003c74df6cf715afc0fdee288cc41f870d7ebbf483ff3afb4fc21fb4260438a8ebb9c24987077087d55080f6a04eb14364026f9480b7e79c85cf9c0a80c6bef556fde64cd71acd82701e584f189e50fc2bca59de44e4e5596f0fffdffbf85b84ffc001488f136a6dfb9e600e4a9597c67283a18d66343bbbcc2f49c93ba2f13a2329686f918fec3360532e1301ab2906617997fc72611d1b8ff7c0cabcd2b218f436332071a8c5c3ce5d88ad48b153bec5554444bbaa2ff2974156d37f39cbd5de7d48ec5816213fd34901069f033be1b8ebd64e8c026c108486d4c835c0ec29707e3bdeb6b63aa1621859e7cc8c26c7a95f3138e6854cd0f932554a7869a91dd4ca7462242d1a287b84274f4615dc91cd197a15b0daee254afde81ecd214f72727c5c3a4b99329be1177de9b5010467ebd16a9da43eb3d113b4945dea7f45d970315be8c5f548813d96d9f976da4068f5e14cc7f3429f4c4fb615d4d6516083735c063cf1078c4ff71d6b067891f8a97c1dee224023bb92373ea5e2e67589c4d0d8011ec10648d2a8abe6384770e4a373e620e458773dc1689dbb5e328797bfa291106c05e6de08c6e29ba95a6c1bfe1f9b1ed383861537f90e37629d1e5c35dd9e193eb33003fec8cead87a7f410dad4c6f3f5d219c373e751731a4d6c6ae3eb3b7c10f3258bc2f52dd974b4c0d3f1e0eccb5f6a308ddbe2b1e34420fc3df6a75879c229eb3a8d47ed4213b20639670f9cd4d1b5883d4a7202aab7c73363e4626a2d4a463ddd8530f03f9cc94e0a0d1df05e697998d493933cc63d05cd7aec28ded12a0913cfcd54d60a0c6ff428735e6051accc1dbca557b6c57a9a8f8698c6a234a76dda0531bf6516a37d23c63845ed11cc08ff93fa2fb78fc1b55f8dc2c16a2b356068d88f68867bffcabf161bbd8d6ec991cf5612fd77deafb62f1c213a2e45be1291a4d10cad62050874a58411a40cc5ae3336fc9435a4db69b20d7511e58b0dc5599a06e0c03bd0dfc9b16754b416d6566c239adda3aa2e7826d9535742b7c35feec3727a675101f0d7f98ee1a39dac04831a73988e46e1b918a38ed3b655c13e2cb222e15fa61fbd7adc8c820ad60ec2beaba91c3298bf397cd557315bf6d5f44d6443ba149b935e44608d7fa523cccf99aeaca8c6ffdfb701ee720317b90f85193150b5fdb703a8622cba416cce94d16b2e04f39b33504e7a19f239d3200bf0d55fec013099441e8a120d54995c78f92378610ac067eaf21ad7f548d2bfad0c33b4f173bb90ca09177929e0980d03b143f71facfcbfb84bb6db5d26d51e3f9b4241f0186bc24c7047b4f1f5d7c186ea4b9c333b8ba71b6bdbf99f2c054616148e37c1a254651e6237b55c83549406fca3862ce265eddb06ac2e46a40a5785d0448a301da406cefcd8393600f2df76b483b11086aecb84891428a756deb9edbf34e370fd9254ce977ccde644f27ce283e85b8c79c5e1a26237438483a6e4bdf46decacae293d8f7e21195189fd781da433117fa99e96c8b54fa67015cdf62f2ca2dee828177541d53ea0c021ef8587ff2f824040bfa882f548b15e70be9e63bad4fd8e4a70368f5ca830cdf1024c11c0b3a7958e64ad1a9ace9bd77cf9676faeccdb473777be944a832c950fa83f936ca7ad6cc4962577cded84e89056f0f92c0d9a38c04654ad4c31e82c20cdbe4b9dc0320f3387c0689123a53630ef45e6fba3c645d5e5bc61baca075b4cc61c77707d7992c16d95b44cac930feb8c4e7b2c9dae803db581f5fea5102173b933d0c76a7e39e827ba13289318ad32306a1e33e8b46f5528a1810f9b9b742acc7da3a9a8ece7221e436125159b4b3fb4f513659bdf2723d622c2a2629ff366a6cfe5708590bc965d03c7dd94f997851152975da4a39e582792c4c80f51ff5920f36b1f06beb9f167b306adc60412a13d97ee0788e12d9908bf89589bc05c30f0fd7f3c210bdf1636b3755081855edf3fa76e0eac4c324fba9a35b68fe97b12b25844739f7506bd53f8b32f5760d6bd0f108fd822e858f8cb8235ac89979664a3e8e2e73068c32bf02320cc2f62ca03b647ec689a2c91bf0aa260a96f781ca65e48bdced7105e70d164410a65d61405a7b14017ca37c8ced3b3c1c07253b75685b8e26e2af52a220e8b58144d652e20844c01ec8364fe5a83a7cf3c65d83ad85ef08fc17be5a8b8f1257c2b947b898ac2702ad3f33108fd51c015830837af56ed9b78bb105b9159dd90e013b1eb62a6d33eb6a1d1e2f2486a356f717830e7660ea70720ce57058b8f86b6c50fb20e77d5c189590929268b45b8faa1ffbf2d5fe3ebf275924c61f75f38bad8c1db52d0a7e51c2431c511b41cf6653b20a8470b4c2404009d7f1c77d5f03e48175caa11f93b107d2cff78d3787825329bd990e437d5a1208eff449d8e1fa5d26580b0296565046ce48b84851b9f6b72aa0ce71f9b68d63eeb16c2697355dc9b9a0d11e6214f821703f531e2bc60a61460ba92385fc592237847c6c5bc550c5aa9f6eca119d693407cedcb03ed8cbab6a18678441748b08561368e58f12d56d5cfd969c107cbd4e3dd8477820ac147291b671dca53a07201a34f835904315f9dbea359295e57f543a4e2ec734343197f81014355f56b41ea16edd25b905e91ae347786231e07f957ec0fd0ca3fdc2a9445c892ebc579e895961bb082ddec5017213be4f47c762df74d4c244eb92c6213ab4398d609c37df35bffb19e8c3ba9c939cd8b35cbd91c611ded19a8e185427607311b0a9520de092ef942d14d48ecb5165ee948979f13503c9f19bd845fd0822935e905c47bf836908b68eff501ee23bfadad385804c1909d1af9dfa6924f643b635cd19a990d8aa3725018ffccb17524ba287a91e9e32c8b873b86fd0b8750ff7a38fb2ede45ac0b9e07b1933127d4848d385dcb36a8aaad43372999c4792342eb04a44d566cff61eaccb221bf9c8920d2ce50b797352e0ae574161d2bc1b46febe9cf4fa0dca77fb17720efba94be87a657127a965933fdbd9d1ac9c384ecebb6457f63db299087a79e39bae42b8a6c10fd216347b63221e48a2ae14ee5f8ccc5e2e53a2e6c3028580f2f0b5484e94b47d851a2f3a1ce8d473ab69bc82d146c931e01871221404d475154227ce88adbc004344fc1f705d838a5b6f71e7986a12d2110444865076a5bd2c0d7d568c0f6b50e190bc37e403a1d50b48d92f549ee57396b1a907b346b1b8e0ec03a43154d4cc467240317d5c35ce80e5282bec5e25e1202a74365240bb0941aa35b161147044681fb4d4dccc48dd7b7ab5445cfe9b0cb36ab6ae6ea05c1e4e598a52cd1d630da6af0a4dc93fcca679888ca2ca963006c36d42f29527bdf14c3bcf6a5e1157f660fe302736cb2691936dd4ae49c0cfc8460dbee1a068d89f5dde4891ffc6a97523533a98b9dcd1d1b05f5f55cda45390433171b5265898f61fda4deb342f6b151c08cc8324cf381008dc5e87f8f143a12e3a1ef37625bc5b753fddeaf06f624f1b77cad0c1726035bcd4b132bf614624bd3fd8648eb8ea49c84daf33ff17f004dad0a40356f86fdaf7985d3bc8e0867f2409b4097b44a6b44bd5b151fbb87aab344febb1c396a4235526133f7b63ee28170537d2e4e794ce8bdfe6922b78ccf8d4b47b03c4d6d6bd9fc0bb45d4f10e3e769e21c9d4785c91a2adf329dfd3c078277cd2c5a06f7f4dcfb7dacf6fdbd2f72b56ae0dfa763d12d678f3173a7c85f2257990dd854fc47a3662f14b0b5c7f4131b9774df4546b39dc817925cec63b007078614a227be794421cb7f35ab89cb2a4fc69643c6ac78d1da0e240883d23e7e719de4f2d4e78ad510f3ee0153774c7b0eca574dd3ade6c3328d1b6c8c0f4ba337984292564cf8ed354ee7d14b9432bba1570ab75b6bf1b5b32c1090cbada1a2349926ed15abc8f538d17e6e86e3ceeb4864ec09870a4b0423da527ec4c8ea0c74ff76e2826182e69529a86a528833262e304c5c6b011d87788ea2d6f436b7747be5c7c470d8b632e88874caca87d5f0bfaa8acfdce497177ccade0446daa60a38ad3d22f29d936131b2de08830f23b6716757f033eafdfab7397bb8c446f6b141fe70ce9dec5cad151237cc9cde4648af3934dfc8ff6407a41705571d22f3a3197a56eed6b9b201a4cdb6ac843c832643eee918ad582faeb16eb1d54291ec64db23255520710e6159d8a5d0830fb75094af0df8186f97ee51f3cf45ec0db9a1b380c3ec4914635059699cff010bba2d77dc64f3e659f2265e0dc3a810ead0e1e7bf685b256b52d4248a0c7c981f4426b35f883fc51987766699d4b9e3938a123e4215c0832dbb7a2dc1acd5036e0a3f5eed0d243e92f830be62ec9fc90621c0a9bd7e653a972cded87dfab7b479c0897b7823311d0e7ead53a8ce20573aba48466a9d07eb41d96207ec27f1d44bce734a1df7201376f2a5b7a229340825c6b00e583caf09fdbcdbb83d7fd2a410a87fbd6cbef85f3d284a0bc4f4fc35c15470d8d67b8681e2a4373141c3d351f0e295612f8c58c882d2332bca6e090cae53de04949228f96e0bc19781b0e421da72698eec863504146fe9dbb580e5d1eb0bfbacdb68a3795c2b020c9803a7b8e24a6cba5945f5dfb23d5889363bafcd3fcc6df270b0b29deac87a8de656fbd87c3ac72f4e6d292cd57afcf9cc240c7ed1845c21566c0397f05d1db76022e0ccadbc50710f3ca4eefb44dc0977f62babb014f0699a135b9b0067b06cbe06853b1b39fab2c2f69afdd31749a05cd0f99c00277722d41541af13508be60919591b93f8afa7c52360e019f2f28d4fbeb0aa1a77d94bd8decc20c8e5cbd2561a1ce03a4f8b491bc1c1575b0c80ff5cfdd431a9da912d0efedcf6bba141443e6428967d92e3367b24f05561c3063acdcb3843e8e2bcf82ac618f9b4117e0d784b9c279e309f7b5361d490ee9c786ccd577735d8c401ae21dd7d8734b3e665913a98f3725610f9324ad495e0a0de160255efbb1a4ab7530c5bf284c0c945658d732594d45455dfb11410a0f60e77fbbe0af4d339082c73f32ddf102e12c9da918f3bf21d55ac8356778b2687c016f471fb9606de7697daa0a88d44b46fce28c2521c00ec6fbe775ad7b606a5f352decaa3c50e3b6420859001c0e65c2879779d0f5fbafe87dd7e9a0718ca1d9d65c658c4290f393c073c0fcee5af4caead8411b108ca92f0ea4c5e709f6fb0873ee283d32db8ea1d070b2282b69e1aeee4f10efcbdab4bb034f708610c9ca7839e2de67128f27faf602ae5d224fe88fd0d2faf14bc2f925f2f5bc7548415a8d688605eadefefafc5fcc488c8dfa8c6b0a5fcaa4042ddd892bf99f661c9d10e230b3f3ec89233d49b011b57a86e88d6b2b4e26d705d6eb3fc8b48cccdbbaea904134ab9533cca8930476b1e65bbbe5ee922817eb556be2c28d0e2387c7887aa012c7dd69acdc2bb9f778c3fb893cbaa3dc2aa9c5005b0b4be857f755ef8d55ee59bd15552186984dc5907b070a024abed2c35ca4cd58ea97935728ea03f027671c08032cc619a3e0a1428fe4a7a523a2716a45366aa0849ed5d832f35a54baaf252bef189b63053894c5941da60ea5e092f93cb58c9ca34d697e4760e35d1a54f36eb572f5843c76f26bd80b44877c0419cf4bb2e6730892bbba2fa72b10be08f58566a99a19556397aa44be324e0efbd0b29dd009f6c812c2ecc0c3252438fa638c9b14f91ce5ab8270e3b0915397ff83b5bbbc05560aaf60e1384f2477dfeac33501b562c70567cc90eb5353669942fce222c2000b074dc4e1a913ee946f35964c9403892b5608e7c72d8f6b072571e132a2262b317df7fd945c4b066d517d42f4f0e65d789ce8cee97334ce3e3413e7d6ece11e48ce5a1908ab59b1452c425607ed026f4fe4474880348aca9e2cd78b1245c8b5ce807545b602137a6dd0c938b58ac9c0845e592c083348278a76591d06b0518707638316cc19432767af31be5eb52bd1b4cf6ba167cc87193c38256a938bd31882a70347bc04e1e55d88aa32ec6e805ec5d7c9f093bc79e46f293d6a157b61df44a1d6db2d6338163c574c77577f81a3da6bf0b2f011bd6e41fa15506178b7c6dd7b6832d6907b4fa2f79950a2f841d577152b43e6722d2adabb2a4a028c872549306097851a67ba078725788e7ee775389c9594ec827d871012a918b29d592d276834da97a732f839eb3707b992a8bfcc06cb879eaa524ba1d22f7621a455ceba39a7f15151dbe92f0c9c09200e10296667915d32cee794f0247e9d0b24ebb9608928be3c7e77695ee21dd51dc8744b9bab18a8f038ba6a0b89dc6be90df8f9a9266019af6bfcf16dd3d82d6403ac87605aab13ab110a224681af885cf6e386861a8ac96f11ffb536e077b58f757a66cddcb7396d917bbd91e3f9cf48fa9c92be2b4cfd6e9c2ddd193c5d5a7f3bf28eb696592f376e3350c320cc94cb8801fdebbe815134b94d7d6e00c8b719913d3876ddb160d687cf033c174e2597e096ac1d332ad0fe0a3ddd8dbeb1ee87b4f9d38c97f971f1d4dd50c18e7092d19cbb240930329d238192057fefcc035f5149571717f9a2a7a980dec4b8b02969cce365baf242fec782fda5503a8cfd407efa0d1047e6ab53bbe48f3391d4659582d38a34d7d297201bff4616be3d2e9d56af0855c1e42737578c6b24de032d94e4924508d84d259d8bc1c4c8238563f6fea374b3a8e33ef81d48a2dd1227cfb30fcd07adf90620381739f26b9292bc8718e6c58d45be3d57d9d77bad1f3869ab42192158c26f06f45968de9f9d574b587fa348630b1ad814fa53e50c6c8e9b6d3eba13d0066442fb4424c327647dfbf147f0064f1e7c81fc9b6e557f8ca358bd214e0825c8362061964767df10a5c32ced2311d9f7603e3bdaa1cbca7bf3ac39412545a9b6282358ec0eab33e6c996b4a05d81840b38dc78c40e25894a082191fcc55b605f79f33fbc2938f8baa4b7da3fe3d179d379bce27ba3cf46dc80bc7030c6a9fce3f16c2c8024d87fd57c33f5cf6a1c4ba84186db0a43cc81be4ea8aca3caf8b13debdbc029d09c1209b6b5af597fb8d228a42d80a8b218ec402f576afb3778d46147ac359e42f6dd01d61fe8a96b39af52028195bdfaa9eb06ac20ff45bf3ec7ba16d13fddc79d6c1e098ea570fb35adf869ae4dc246427f826652f0041fd44d7fa690db55ab78fbecb678103c79ba2731c4c8ba01e79b7338715c503e9aa55b97d811f196949447e50952397bb1f1fb14d66f456168dad06a175ce51e3061597fed194b93a9a1159b523f2a02af787aca1c7b1554069d47324362ea007bd0b1588e8f2da769c289a598db458355150985e94babf56ac069d832f7f1ef1990e82e00947a70dcba42e9a16712fba9e03d1525ebb19d73b0e23cf350fef801a753d99bdc5507a6d1217486ff70fa728cec5b6861ed7e8439a3e94f6566885948304b4e6637c1a0b89cdeb130a42eca1a17e2641df7ff51e0ec4430ab0ef1e490edeb8727bf6401f4e1f2abb845d4a5f037ac82e799bcf5b2e210e72197dd4e43032196dd0abf94631f691aacf0d31b92fec8207ac504548edd91ed94bfa77fe063883e73422c476c3fe246e4356b7498ad7f6f42da01fb3632050acb207c4bc3e707ad73776b4b6f3c75cd91eefc66a496f0655d40395d52d7be94e06ecc2d2505940748d4e896928d0c7e0860e1ac6262a226473465eb126fec8afcd7ef294b21a1551efcaf5e1545ecb4d25a1308cf9b2d630b9b148bfeb50ccc32f6730f7893343c54be5dde187542ec911c6e12e066323e0bf25d3657ec2a7507876854594de3bf6938fac7f7c4e8684567b47e41fa15b3fba9e4f30a427c864a70939c51686745db0bff1f40357e5edf3ff4ce677980c66b74a7e0888c78992f3e3ce3e150143b39907f289bdc02ffb2b574d8da078fd5df57343e2a431d4a977e073979ee87f9fd5fadb965b5c404a6dd5072de6b218df131e26b52715bf26ebed8ff93be2e5789bd96039264219a21b35210704efbffb275b42a8cbcffe65d2cf542b1b8d1125b016b29f92851bc7815706330dfdf1ad4160d685828fe4bdfa1a19318be5d640eac3fe35902f8530c6e8da158f0a70b0eeeb411b54ef9c0552cd8cd79f88e493fa21f3de9e72bcc30b249eabf7796c9e22413f64c39d490441cc7dc6eef4152a0537dde07a066da3cee0f3620338d033923a2df0e9e7c0c4d1d25e8852c78bff567e9949e127b081be85fd72144bda3b6100c1f26a2079495e992ab900faee8f43248f339dab328f6f92a7321e909c26ca521d3c2f967a15e7a7e64e39d8b8b7f0f882e386706951089e61110aac832df903bb685f2fb8a0a2085866f83609e0e4b143a219d66879a35b9aced182597c1fda7e989692643730860d1b065c633d9c080abafe4e90fb68ad55ea22d27380904db4d5bfde67cbc735fb2b2955b78c578291512beb92f5373fd1b5b2a0efbc848b8e15370d6bd81c664bc9cfe6394e798fe86c99f4d46be3af59d507ee00fb789f3294ffba3460eb215e3631f86c5c6e3a8180433b0a3e8a14f8227f6c821c11b05b6f2aed4e09adef9eee214a4720182a41ba90884c580aa19da52c39921333f3371ef30b3c82f257dffc6c642287bf71902c6c3759c52f138c917dc7be605451d1c28bdbd82805b59053d9ab0096e3ba19ca2192ef7d5d7fd987985aecfd4f1d0b38a798031fc7f8c79a47badeb011aeb52c1ad50ad698215356b89a2a79fb238163a881e9eea8c57b37adf2e9675ad2274d807c7de25e9b12b7b4fdf9f5fb4b8c6f31adc13600fff39550caf6338662797f05ecba8728e68125cf4a7296ff261dbfeacac13bb61819d9e265ebe77f020b3dcd766dca7e3ae2b09ff9e90a631625518e005d5c7d156efc27f5639681edf7c83fd6b6424843c2aaff57ccb34d0cefe0171819366effd9908fb89ebfee31989c19407d3ead5977507bd8327dfd238db869013f428bdbf00eb08cdfb0303b061f87a4aa686bc68b7c5d56371d482430cc71447ddb57a025fc277bffa7f8b9ab53de8e10b2a5934a839c3e7ce85af6498715e3199406ba760d6e8d8976cff5ddaa2ad9ad4725138f68bf2fbf7c471e296c0c9ea73576f353d1ffa44cdcd1e6e4f0e98417af65617180d146e9ac45a284dc8ec1344c99df7e667573b203c1b37d779e3a0ef2685d63fb88e24820606571846c02697fbd2b416f1658b639c3a8a43b69dd77a80e5fee12e20c8aa424624fe58d769e6e63b3880afd1b6a492d2e54efad89c238c3f4eb2b7251351c436297e2a34cc9ef80cb59d27ed1deca57c4863d02dec0a802292785ad20c612e0fbbabc34302156e90fd949a7d76469e0d4333211b25e5a6ba634584b12c3f7a3795db71a8fe052d8d458dd53e8c7dfc313c41eab7abe86edb2dc16aac50f9f57a87b2e41e26b238250c16c14415b55d1c691c024b4176a6b890b37da1f10b3564554e32b8a0b1c998578ca067e5cccb43e8b869c8f42de62af77cc859555cc5fbf65c6ed007a5c6b8d3e58a698fe2d33fde261a1252473f3509ee8e183c8be6fc519a80749a92094898b32bb78996978fc8984854984d67ea43b5fa53afeb709c8008be136ad89ba5c03a0bd2f742dd8e9dc9e73a78029ba553c66bec6daf4d03f4e96bff02891d0b624227956f8c70a108be04324d49eaa501d8b1d5969346b1d0d026d39ce408f750195f64c189ed7d97578e9bf990dd17688a8e8d8ce7c21cec562673d14060e670093d3eb4d0fed6d0deae0f7ca51d680829847c6d204168af50f2f8708033364f759702f9c24961d01e4688c609ea045d77474452f36f34ad0d13c6db9731fc8e2a405ef38b2edff755c603b09c8196e8424432844d3c57c40395614bead48560eebfcd16ab29bcbee02bf4a608a55fbff445d911fd150274cd20b2e5269f84e9fe4c379de039a8cc157eae04c8ec000b0f18d3f67c64f7b11b1b4274323bfc38831fb1c18b33d4a6e14e01dacfd8862df947b6da2a5aef6c2195374dfa0b1d888e931e79664cbd2922b0ab6b245b34e9c290a3a67be4178e940ef3ef6336924c9b21bb747f6e898e81dae0cd13cc94b1a03079f5e8bd9bcfb29960167737fc11d2b6bf31b7dc150e7373fe0cf16fd72ecf02daa2cc98d608b2d0ad7d0d8f1386a457a9669ffa3f3ee22ed4bc6379ac0ccb59a85b2c97d24558cef3952127ecd498de0755a2b95b01680981867d73fdad5db0a30fe741a79be20a1cdfc1156a530f5f32f83b13d4eed249714d0a23ca2bfb63b94fb8ed62a7b78d2ed7b795586d1bb2e26117de7253de61cbe1c49068cf1bdd5675ebd37eea8c68735426828aa4614c10f6642671517a86b963f5e72cdd65af704aa9ff16e2e656c189f0fafc6c3fe616a360b88428e56067c509bbb934bddaeaa4c184a0d41e295f26cc2bbf5f2e22df35294c7bcf17594d31dd0443fd817cbc82db3df5f73f14417dcb245bb3b007708d2944db3532855f8e80d69e82be818baa9292b8f8eee21c0aa6492ec43736f19e0e363520e44960c04edd31812415e1d61d537227d7b9ad7cf5837573e47f4b1bf7e94e783e2fdb4f8a2880da0acce945f05345d34f52d2ec0ac526b696e21f7827a94c3ab16a206c2eb51dc23bb3cda5d7e9c052075e21fdc4edd816a48bc1410fe76145028eed6424e6114224f836cc35c73be5a2e36980ff2c66d292f718214930dc4eb11a0125ce7d53241ab196bf14027bf68a696b9130b1fb52b60290277c06e48f84ea3770291aa7987120def4d9c8015e2825ce152e9a22fd9577807aaaebd4b9da08e4350fcac98d91ea617bd57a4ac465cbb0be3d013709be531210b2aac014b1c70e6c0bb62f0e2e83bcccb1d3aae7f8ca07330796860a3b5fa816ecbd3a604ed0da45ae8096765554d4d550deeea871855cb78ba980f0c8063825b3c45510f3f8ec0d59b9e8354ddca2218b2da3c2be54a6a709ed4d0b339925c30134a3ee2ade567cac7baff64dd5ed070785933e0f7d10e44cf60516fa2b51964f944006c7db943605cde77653e876440e537f74a7525d1a721378f7f905c2cf15122e50b9ba907812107272ab04ee83b757a79854d3f6285b141b9e9aaa600d94c813a3b832ae2594c075be19afed0ca7252e879b22176ec1ede200d75c470195613ba0e9cc88ee6e34f8a4d23b3395f86cb62d33b43645d8bfaeffa998e8382d5ce61b780706cb284953b291d00a59dafdc599cf86adbae44f14ba97518d0cfe722100e1846e2f6dc5ac33d7da8721b38e1c15e7a476c50803a1d0ebcf880c8d52eaeecfc636b1769e30beaddeca6c1f7a0a43d1b4f05e0629486d098261af6b31a9b05e60de45b2a9424825b622a1c058bc64b1f8716d7b7fad14977e5bdf1a829e3e9683108624a5e2f390128416126690e045444f202bde4b75ad4c6bd091be7821a0aaddad51f3d5014568d3d40618d687dfb7ba921c60d879b58e14c5b43f7d2e91631facfee23720f7b2393bfd75dc00d77f1d993753622dc1f053f2435e95859e538b9e2ad71da955ee37f0a204bdae095ee60a3acde85e2fdbbb3c88f7abe93a511fc85f38c5abb308afc15b4376159f066245bdd372dec5a78ff18a4d4b1c7b5a0552a8f0b4a7e0d772b877c51bc7e3c21e0aae012a3dad1744d73b5a524376b32aef05d582c0bf247f56822cde2fcfd43994511535bb1a9d83f74619785c287ea6a2b4f97c23b9a082b927c35346832ce052df0f319e51f9b6b3905a7b6553d8d20f520611eb10153ab7dd3d000629e2bdd3184c4dd2f33931a36780a11c21e70e356729349c31835e1ca68d16406eb8b88b86b689d08c64822e45925e916cc4ef188765846c7276dda2c953c6cecf6288fc541f546ce9151a7966716cac0e125e5b26ee7806d0fcc77815cc2fa1b9cef4c8f0a273a8f6c61e4b3b270aafa73951fe26676bdcb590f240c240640e0b1560eb2cf8931a9a988f2c37ceaeb5cae47eec221fcc23ef2d45d32fb6c036a50c3d7077e7a5becccd74438b15dcbf64260b27167b5463674cdda75d0f76640484abdea3af85ce8b5bf8ceaacc46f4a4fca37bbea6ef1d8d523e80f5033d472c8d9180f3126406ca09643742948d82943ef05ade84747c2ee13dde97162c1abd9bbc39647df4bbe7f6799abb1214246e3e49cb18a6bf68adcf6c88c6ffa4f65dff2095cc576d96b0c4f1818c8fb15019543f76b7f38c63a271d591a2ddbf514b0c67dc998b6b5a7557a2b2d5e29f127adb5655d4a9c456cf04447d37c6836dd793bb3e65bdfc83be216cb6ee6322afa90ce569108d176c31051d1ba1e3bc16f0090494f461f77e754c82143dd2c36d190517be7bf07a6f2a7f10e0424818539d0f710071cf71578cdab7ea918fb198b64ffe82ccf8ea72b1cc39401b366f93ed969191e2db8ad61ae68d83237300af4f517e5df0278cd025c3ea7fc421a90135afc3e4a2908d2c0c46c44729375937e71bf26a6e4b451e3feea79aff952f6238baac49115c09742f37fa087728d06a2c94f5d54c940df9a6615d8c91bd1702f73f1f2579bfdf1056d4d45a72814b226b066c9ac2a4f0f092300e06c4a15601160bfed7bf7919dc018b9d6e7498c6d2df8d5bc4b5ef1c52ae55f35a72f75b899df576d05f242a3d5dfc6d5c22db89a51271326f3434fbf88bbb6dda6a935b6a95d4cd1ab8bb0289f072b77235cbecc248f9087162b43a95068ac2cc8f0ce845a7b21ccf0333400e82449da25734c9a7b6fff0df447b93c34a4bae40f091ed65aad7916387618e4cb6c9a7afe25626445485132d0539331be63dee00d5cbca803818d25aefb6d405e55e5cc0b21c80537ad5357f13c52463f7506903bda7cce79695ebe2c7d980aa2976ae3d48677f6f260dd1d48251e776ee331dcf8cd23c61596750198bb1a07dc7b7c8c9c761e7d098f252dbcb6cbfd99f2962266e5c39aea6855ea7363a19fae29672ec84a6b2d91b8d3fe527ecc6ee94a42fe1757c717d8549b3a8f71b317668b10b1ffdce5dff515b4dbf2831f039ed9902a8e10eabcf3331f8f5cc39a7e9951b162a26834432736857c36e319f0ea5b4500942a8a4b998eda3608cceec80f62d60a62a691d9d21b4d607be56ac92a114304ee5b4f4703c7da3bd036ba982d3e347d829ce3eea43bdf84cca61bebc5816149f38531cce99203724ae93c5f505d9730d6a6f7a22b557b3d1699ff64de66c40602b5dcd493389af2c3dbd04196a666ddf43309c0a5d571425e1095914bdcc70c1182cbdf0c08a673d827f76f453a12bff768426dfadf5546b24186f3d5c2703da0faa50a0bc1a65e0afe06b6dc8df49be80f75357e767a316f010584edc6347b0ca82253d8d3d2aa7ce7836d10c27755472d42ebc1ec6f30bf582e070dd35a053fba54d2a490c244d131a567e0128872e73702977bac902f697eec79cfb4b2adf881976e31f4dd40218c94cab4a906d0ca0a6a1bf802b3140b4085b1cb0db010325dbf5cc2853a9bca0664496eb8bd284ccd59d5eef35396dad19c45e6964d113c8ac2f9382f0673905d853b521fdd51e2c47382e2a0eca4b20db303a9b3e5f7fe658648d05c6d1b61c2b625040b9180c58b2a9d6c2cb052e3cb2e078ec7fd862b09cc7daa5553f9a25d95c42470a3efa1861448069a83a82284ea8c57152efc46362280ce8b5767eded72194eaf178dffdc2aec39472537edc20be4725b05f31ef0422eccd287d8345442f543fa66dbc4f2f8f958cc5fbceda89d4af1cd09c4a0dffadcc807502d502e5bb5e1556fa019918ce894cdeb02aa4d759011339c82a98b3899569a8dd800ec75ad3e914474735dba471b0bb60fac4f0ce3f841a86db48b187d484c51342fa5e119bc2e7f09d4006fe184f1933d2af162c72b555590a5c0f7c1479e9b1b4d86fe1d97eb99152c93694f786a2b5882557b8af01ecc74affbbc2db73f4bd5f15f10dc5f43ba2587cfab2282359364d61266b40c019835673d3556d67170d2627d4291f71bfd7d6705159bed86571c2ae390b00f657d41ceb56b4ea067a554621c649d996ac5b9a466bb4fd335b1d3aa7abfb1a45145a87f4076958322d3bfc93f79553c1571a16983fd96fb94f41961af7d9cb7b1cd755f68d74c6b67d8b11eb6b612d74caefb43bf31ae18e141fb742e3b6f6310e85420976c40c2a440ebac420cd81230b922da70b499be9918a88c1fd2f5c13b1cdb24b259457fdcfb1fe158f6d0394ccf5a3e2f38c999c8bc495dbbefcef298b8e3d21182a3a63215e20c1325e5ef5b09f65cb85c1cd6b87026072586ac300fe2918bb65747ec14eb464e5af26600704ed5f986392678a2d202166cd40e2b27e806ded4a5c42e687631f7df1b50effab5dd3a3adfcf243250d0908324e6c1439e282cfdc296d4ad9959e8e37aa71669ad286368d30d0285bf3b9d7702811e95acf4cacdc7b68a59d3cec7bb5008c3efa424fd18659b7ea6068de802bf43a544daa644fd7c7f0c47af82d52545ab704adfba09b95cb1e5f5e15d8fa41bb385b328541514f26120aa81c96d849cce43f38c5cf199f433551dc2fff7dc70aac0b64718e49a2e7bca99e6ba86da3a1094e0b677c303623aad62a9e1462c2526e7f9fded648ddb87e88587f6a4dcf75ce284f212df21a9114cd76214d364c4e896619e671ee83f6e947d2a81caa118de86d62101780cb6b56db453f0375075e5586e931cd81fe0aa1ea0f7cc90f0070353b5b04d945c003f40123b07fd0896ad75f611d04dcef8bfe2954f1f637288d71d914d9f6af42a6c1c15c42d01d5fdb29abe79b379d8dd81c3009e01d042bc95965e65b61ffcb29cccc780d15432738bf71e7b2eaf2b338442d2e2172a239087b676b6ea65dec816be5919117c45d344ae236b659a9554421c8c78a24e8b1bc2eef49e101c03f8067c690fcd83015cb4e692c161605cf2441ad74dc98baec85abc793fb4da9779888988cb099e49dfc4a4ddb39951668765592a9b3fd7396ef46bd66225231ad356baaa5995e27a19ba986e57c75f4d9943c3f23f4d6eeb3bc2ff9bd0b5cf4626eb59691b2cbc192ed9269d8d897059b47f2134b82ad8dc03de06e2e51867a79d73feba23f61573ef667191ff1eb0dfadc3a4b09176614e7af27985bc18cdbb3fd7158af6f6b882115f70e07f7f5538fda4f561dcca230bcca3fca3a21a6b577e3281807f0b404fe746bfd3391a00cadc688794e5b085bfc9e22e4b5b4bc3caf6244096fc7135c2bd9d3d633aa8aebc0819a4671a637b0ba9c322344dd757369cfbfde61900520d25dee5e1340861fcad787217bb5830ef56ea638207e00ab5e0a072da1a1f86fbe65b95579fd94c71ea05e9bf1f95651c9c9171c69699c197335e9971c6cff147ddf611fd82ea24ae18e23d97bdf8bb55a85bf2df0770c9dae56fdf77f9a96dc2731d648562b5631897b3d941db9490f154a733bf53062a01887487f0264fa2bc4f24abf8eea453ea37d123ee338809fdbdd582c65671ace7feb9431cb40f88eb9056f3a147786708b93e06fbe1050e2c99b9a3f4300b7ab73a8ac9818ac5fae9525c3f678d968fab32b47bdadeb9e27d719b559e92b597f004d7bba6e1f08b02b5fd5197640d15f9bee2d1ec62fd0d4f1d65709fe25ec20f502bbdcecb4aa96f1f890fcdf520117ddf1a7ce072f445093b7b04414ebdbbd1bb6db57e9c3ab447ef0c927dbbf9def6b608e07d7d4f43ab8c1fb5f71a784c075dfadc90ab59cdf5a9864f37027b1816e477812d54a88a6323e1338265b86f069d3a57726def2bee1c13088ff44e94af9ff40a736754bf68e4e3e47d65271fd30ed041fce0553322504678d0641df84a523747ada54e6081521acbeef0ecacad8b0e2e4528602799f4778b1800fd2539456d80f589f30e0b6835a195047c22edd7ff7668f2165669dafcd21fd61c304aa244bef4caea8ba86cdc9f27c127f921cee7c9be9ddfe96599e5037807e678d54bd7d9293ddac1b2b9d78221542963ecb05f5ac2e6fd7b2c05fee4033d79db37a744b45d213f63ba923e41387417f2e8f5c8dbfc32e625f44c36e49c9be0267e02f549efe2acc804f6a88e4754a5eb33139487baa1d07111d0848ae157bf002eb88aeb1dc27403cab04267bc3d167cc4772c51626745b09a4e6c26088e898d792c25847851480efa4c569041e639af9a3395e1d776229a00d43d9567443b59108ac4140ca16549f1f9c1bfbdd1b1a10745af8feeb3480f56afd9d47e73aaa175c31f6fdf9fc04c271704116868620a49b5ddf9f7556f9b0d0249d6f02d5c336f72984496b3bcbc628cd4dc5b846bd00e0d1b36931d7bd4a33b19189ebd150e0e563f53e7696f37409ad655a60e8cb638a3f0e50f585cc5081d71963623ff649ebaed4314b2f9bf328e54028a8811e027236079dbab79bb16ebb21999f143c6fd69aacd9af010b96b8f80a59345452893aa3fd514aa8d39db0a262b247fa255cbf36d0563274c66b0d5df7e576cdd36ef66f35b8ec0d254e7417dd514183298642a52c94d0d9349f9ed5439f52b14fda4c9a9522d165b494a9ea4bfc0e474e761b010a3802f4fb8a98fb6b8b51f4267c13a4deca51d5691322752904d62d73030c96a3d2f96d3f2634d7e1f7384c27865d0cc8450e094ebd60381ed883c8024972e22ac16f3d92012a4e989fffc53db19bb302cf8711d2f37f7ccc40cd36122a0bb1f42c9600c545e18156b47dc5f50f3cf6a8e242a46526d119d408a0f8e4f92c89b5e619f60986dc06a46e00fb432cc0489844af9fc6cbb5f2e8e84a619a4707ae32d30b81cba856b0319ad812645ad19314405b358c6d0461c2fcd9df0bb07f430c4eda214d7041498d12bc7d4a5e48f554f25df42ddba16346a0b10515b2fa3b9fa9706b033d8c8f6fddc4d0186a0b3d1d1f9ef8f00025d56e7978415f7bab3e90cf21934602106d1e8c470f1c25f19b1870ce943e96b6bb94d25932b192b5cb92cbb209c34a78c0a85bb6aee6b3b19e26a87f66c0ec20f5a821e454531db0829a8c2a7ce21c301375ce31adb00f85fa140c12d8c7bff2c9b1597703171ee34f23c786ed5e249dadf3663fead9268d7fba29f32b09846e0e3b8400f4bea660054b8dfa0a08b3863ded852b5077e93bbad8fb82352ccbed17eeef42ad2161fde5eba6308a189a78581895635275698f40fcf38d5acbc16e188eda58a967a7aa91d12898016ee4a732050a541ecb9c4be5ace9e2413c59db971b058546964dc349c72bf4e2950a53439900cb421e6bd7e2d697c8da6ffba68879e7fb1010389657cbe735727498345faaec41dea37715252c1849b82baacc8365f785e8287b8e514b1204e587dd666d8f8a88e260d23e5a4092a7d061ad40773420c0797e5ece19728326f62d7e37c4c3d9343dfb8db9b43aea9b9208e4710995cc3ba46c8adb284e5a8d1af12bc44177ffbcb2ecfeb34af3106ca33e32c79000f3961614ef3bc42ff33e595919807455892c292e867a6e1dee8e18242462410bc1ce21fd99e522055ad1b92dc9e0b2b7ad4c78b151851591447c6d83d4e6cc01335b9f545771332f440a3da37d9a444f402e8711a9e45cd1d3cd08abfa4ff42be15484deff1f4f50db9166d249dd18dc728b30c9ad9ced3c6051913fcc10f0f7d6695002a0e03ec9ff380bcfd9495d088ac6c7928591cb9d4fb7dd55a7f3459929582aa00eeb7206ca38d66a099f0964439281f4b43ab1f8ce24e8f8bc0634d5c93d7f2a18f577f3fef5604ba0fe1be9baa856af5203db9bdebd749b16aef48d46125e3bf91cf3af738ff2ce09d190292878771de4a0fc4b893d404fd427bb23a0945cb30a02b0c47d72600b03c71fb1574ce62025bee6ef5104cb0838ff1e46e24ae3325345e7f1c98dc97e665d61666a1a04510dab83461b2b071910aba69c5ed4ae6b5361264d5f4e7bdfafe03a0d50807a42885c8bb6add64910c04b0d6bd96dce56670250ecdfcd060e4aaae8c646c9b762abcbae4f5ae296e97678faf3b0ed6c48189fd33efebce804d2564e242b6353f215559debe108f985f800ee5d0a2fe82150f292a9cd244a8c85aa418b25ef3b6462766859055a8493b81227a6294a0c29e2439cf0bbcc8a6f9db490dc8253c4d623ad7fa5ae3b2a96939518c3d808bd97af62844c10a80248b5db18a2a32d00fea71ec81884dda6b3de563c6a668a4cce3f32a5bb8342f0ae88aa5ac64be7bcde022c272b62aef88391fbbc6ed7637b8a8aceecfdc9effca928f2c9839848b92412294655d05466e00bae74748fc053822ee10fc35eb3906eaf0c254c2f9eaaba5c2dc65c691d2669eaf10ebeafe1c474c72cdf2180088f5e617d07eab79ecfe60fbd862e6338b6a4780ed8a5d5624e3a17fbdd9ee2293f1045906219565c91097f4eaf23e1e97430be486bb361cdc3d5194d292286e6c10c0ec39fa56f995f0ac77a76d3e093182222e39826348652e20c850777d261a869c12237ab8d12de8eb7831663c87d10f25ab408be56918f43f6279961c6ceb69652c30ee79845f69b7da13fdc1c039d4847878de8413f73209dc4c4bdef115b52d410f24b9c963c897bba3d22b02a27ab2b6f8d5c88b9124de19e79be8c71ed641d2e1699b15e4479181626f3931931c4c42abbd9941a716aa2cd383a92bb8009659824d69722a5d41a668157232c9eb633f88d94b61eee9110be26447277e30a19ddbac9eca266fb6932c32b3067a5793df61428681bcf26d3540504130fdfb42e4adccfa96766f1aa5a17faf1e551b282aac2de1876e7d0c72da2f7b77e4e8f84b272bd16e81d5a6a68ab40beeba6e683a38cf3cd20a2bb11a72707e0b1d5aa61e93a2b80c2b504b1a0641db744eb1487b658846b1bf87850c6117380450e6ee2c927aa0e6cb015df401c699ef9c647f20763296821ea0359517c2247f669826dab6785f49f65e96dbf15ee80744012d90f957509d04465c9f06ffbd7442a01b0b44c3b1ce36cdc630afb3530466903526732fde674abedbad50790e890c3c6155a00bc91773e6b36a5c1a01322fba528f95be30229b981bee14c74755857843eca0763b2726d956b28597c846ebaadb732afb28f5203cb1570f0a18629bb24f5eb0413f92bc07f7103bd05ffde31645a6d2698f921f016ad2c95f9b4086695b6f4a79eae013e3bf9bb4ff4286c4c9debb848462aecd49e5b9a0dd48817617415996efba73f372af2b6c73e357cbf1c92e5d5a4cfa35a395acfa95187baf1386248d9792e27b612f293f553325ef42b57d057b9d33427b7f75953670a1a24fd820d6816e67eaf4e82f7cbba512e2ec7a991b52b083fcea687ba8d8b27b97fea8f6eeb6435f6cf4b934923e9a3e2e439312021810993ecb130dc1a3a07d9bcd8ccfe6b5518c3b08e1216f0b587ebd7b44a13b1f498ad904d44087bfe7d2359c1a64e4c26aa57c507140c58de263c1b3871b729d04feef6988d3f93513f051549760a2d6068ec75398a30665b007af8fe0a3426a36b246d7756d4646d035fbf2e22d6adeab96bd44ca87f1ca5cffdefdf532a275150f92deb7c33636b9ec5d1c8c288c61a1358f00c4968ef44e7f44cba8365eb8269e8bed54aedc9f8d9ca7cf8c0444614aaf4039726d1dff373d79070af10626142c94d913891c240d6cd31c4185297fc92590f3f9a87a74a74c905b13a2e59f1e5012b1bc606489b0d4fc77c8795b2ce88a9f9fd7a8cbc128e2ca125d7c82c126c621be92d6af8b2d10f9a3008c73a32a9b53e5ae918b014b0a2a89c138155c64a4a7a84c95a3f5f9c67545fd5c95e43ea3b3cba24fce97df77d3a13da50f0ab0ff0b268f0c1a3a7528c9b2e56f3076c4ce3cdebdc1f6dbce4ac4010e04d3483af5b4e6d049b2f821438e64bc1a479478ffbb634759e57c0ec51ae72824009e2d7adf7a358e2cd00e3ef2855bdcec8af6a609399cf10cc430c8d47fc50b158f44135f49c26c363fdceafb73e824bd91c7cb7213233acecc1ed535ddf61d12d28e9e2f7a9c89669be9af15d41753520c7021a4a2bf4941559612803f5141b7fd3790537a28881d07140361033a2e8364866f1a737ae20a2ed1ec5fcf408d694105fabb14aaa7afbd5a41c12d3c7130128ce83a85cef46b6d6b97e372083aed9d90856aabf10e6fe7f2bf99afa3e54118adfa0de333e7c6ef599de7f7a8df645f46f9a6d370dc12cc0f65c51f1e31142f1743364a376a94d804336773a371a266a8e783f0690f4ce6dc31820f748ba14a461fb0c618e048a9d3ddf0c3235bbf26d21ccb6a109939406d9630074b0f127ae04cec308ebf506bf7abdb685227190e7c541dc8da549a1a6106a73d5c3f1f883ce8bbc2d4f341cdc36cb5bbbb7f3e0f22537390ed60f9b18f18eb51b9929de828d56ab560b9ace3d752f30b9fddcd002ebc7ad7f851e465c9b01e5b44f0620b4f7dbee6610722af175e20bd922753de93d3335565febb27717b1ee6f7282b83e161960e37f62ab4ebbc8d74ff907d4d36a4a995f1e1cfc91a88f9c3a0440be86f856da1e2d82c21c9c00c14ed8e792550d160c1c059af0f4cc5a305f05922a82ca575fdcdc315c9f3b449f44e2f9ccaf1e4a9827cd04c2d4d6a393cb27e9157364b39e04d18dc4d7520653641d4f6f5ec7fd0a79627feca55c2d9e175af086604789665a1e4e0abcc9ebf033e33c82e9171ca06e2785e85a02e68720c9d68ac60aa89266ea27933eb318f4d6f6fa8445f2ff86c0ee48e690fa584a778573cd3790b5569a4b87464348c8648c8fa85a9b55c4ea32f19c52f56be42afb2bf58b7387d61287511b47c151d82fc455588f4d3e5f95e3b36ba7aa520bba0eba4aea0cbc483041064f20dc729b2e17d19bf3ad2ed9c12b74acf035ee46125cc5eecd8d1b81032abbd344321ecc15ad0a0c8ceea949f2a51b686f868fbf1b6c92250f9de8d08f871e1edb0986b00a132fd4cf12ef75374f6b4a1d8eb26f59d2682beb859e02de869706a769187a1db1f9d22545694f7f86406bca09833d24172539b15c865512188b3f0e850f619f1d3f3eefcca055428b5c1a603d262793427cf9c01db22005ea7853378666da5ddf86f521bb22a81d18cb667ddd7668accc966cc7e765c99c18fe0eb10ff103de5240569145743bd261b27b9576c83eb7d743ebaac09f6acd4701b154d7067369770b9d5fcd63b4df960c0c7bb384061ced4f042dc5362d963df1b0d49f9be2889c1fc635db8cc0e794ccc04bd03c92b97f115c9277a6baff8973e223be8af98d664d1ec42cd6551fa15259e480bdff21bd78f5bacb38e3ea7478e7b14b3912a72de85b73e6e2721b1e0ae43cda66fe51eadb031530bd3d174a034939921b05416833af35d980655f23268f6c972218e71279ec87efa12b8ba740f2fa9590445f4b1ad1e83846dec04a8a72612904edfa502c5b0e4f8256971b0a176f27c3961d9a0a1c0a97505d38742cf38c4cecbb0fb5600e4079e32f72540971320060c46b56e997cc2019bae96b2aff76c3d6c636295e3547cd5455039ec9b87394ad4a1946fd7b3520bb7a0b1ad6644e1e9aebedd59dbc4b70a402551b886c111ff14892e0388ad1ae7929fe66a69016a1a61d9fc0449fd9f13904b3683b03c311b6d7207f2716b5be166a9929947ccdaa1a2080c8a6982df4d82d5dae0d90c00512e62d01c14c94f9fd3d2120ec6038a7b1b20d10a5100d873505ec94817603bd0aabb2a73c316c6e588be07cfa75b3c9824bffb3059dd5da1bd7405ce19c843c9d10210345548d25db12faf8fab149e154e486d5fe80dd643d033e1ffdfcfc73609ca54ecb70902970af9cebd4c30c67cf5474195b063cfbaecc647ce826a4dfcaafb041626d9574ba09294ed67f3f56efba9f995cd894989e10dddd330e57e6e66633ea35660c440a36afb32fae8b8517fb84c175ceee4c112d97b4a80279b239a603a1974dfa203e9135e8628d17e18422cf8c3509cb8f4845de65fa2a14691b7839905cb419a652737239804d18e72238f2fb9685b6d65e0d42ca4c3dc3fbe1130bb0ab709e29a98449eb1380967ff2715ae67d0687231e8e1bee70085552c3b7f4e733d74b7bec036ee130f3692ab5d80a0feb476d6eb69b233b15e7b68c609483e01c726b60958dc3fae678e8088d52cf7bda4b80cb8e85b9ccbec656b06b534e61f84732befa43049f9a1a0e4a512ac5c689efbf1d1b9c344a3b45e10e6fc5650d1a6fa838be8a7dcdef33251aafaabe964a76f9c9f0f82fcb6c58d276178d458dfe73fa10486e5fb0faab96e7e574af7b0e14618bbdf7979838d39b9cacc1deca656b5e79f186ed2b1871df6b41b23b6e519333c8a0f09cb031ade4a4330ed8efd48025f43be693a20ad3f32b85f8599f5e4e8080b3e892b6b441c56862b5b9691afee70aeef76f5537895458197ab9f03f3501daa196453f31e318dee83a591f5388119b2eb882f8315223b882521ef4222bbe1e5e41b6ca9375f77856c4bccae45fb184b1efb65150b6d372dfab512a3b191e77f4458a52934358f098713b4bbb813a5aa9f0a584eb16876a361573f254d8b22b0cb6cb5f1f95c2b3916258a45f6da5a89e8060e2a6dd3f26771fe241fd76cb9f136515eea8d3e1878690d4acf2bd9540e07d98972ce48b4333bc32c9152dcefd9414eeb9cb15d3526a74906fbf46323244c4221706868462a4068372351d77852fceb9a4b9027ff706d338236cfc1a8a15fc1017559ccc02432d2480e3fca59f1471f5402d7dd163b2fdf911adf473da68b0a46bbbab0f02f9e7583802d6d36af7c0ebc8c6fa94e8b9918638b31c40401b513de8c1ab2cb1765a00d3b43be725d903d9a418a41f29506613f98bbd004ab1990525f7c5eb14ce4aa8633912bacd2bc31f1bdf3860b58cb4b1f80499e14ef18ad1f699478a6b55f72bd859cb2acb161922b6252873b3f0f52e080ee709442b9a0f5beefe1c29fcae57423339275b06f884cc2c1737f38f4f694ac08ffd34a5816a532463d615b81fbab7730a9e5f45435135374ecb91b2e3f5503354dd092e770ecd812ce906178937b238c3fce21b54f0098a3f373dfa69b026996453c2a40e1bdbfa015d0c4b4d3a7e631b52ccef34d2398c9082e223a9615596d22e8992a3774a94475960ead99fd0cb02d9d89bc8eb2de777fe0a8c77b4cbb034ff729fb6bab77ec2737d10e9c6ff6acb83429412dc3adcf37fd5bfb556bd26ccdeccf4230ed517d6e2deb297e1f256f56b0cabac59ce897eb523c9175cb5ccc14a32f1c3784048237c405fcf38ac3bebb0419ee87276c40dbe06f49ac3f96ae2de989c7dbc74b2f15578f9309c2f1de1af8297aee2d6970117ac9907645abe461062757fe0ee2d72bebb482ab09ff27745011a01049a01cf7590b116719c3d92f3d131a8fa0a4bea8bcc27a1eaf1139602637dba4033a8f1d715e717bd97d6590055a82c98fedf25b1a00b5943e235cdaaa6317f87d2b36bf27888a172a9ad2e2db47b4eec519c9ad05d8a43750938c1868fec2613901b05009dd3017f249320c8949380cbcc7856430411e1986c2459303529f55d42552bc270d8586bb1055b73aa36c44fe7dfc38b4121db5dc8b8fd6fbd05ffe19514124d9f204c2286036b200ff16ff90b936e7f9c046c81ceb8493e75b3083efebd61ec8a14fb742d7a3119f8a31611e7f4f762855bf75bd94d9ce71a1aa7ad88c25468cdce54183dfcbb7854a00130c63920552ff8769b2e170d4525dfe9aff482bd265acacba878232eff18662a8585ddaaec3e3f45de2a42cf9318e84ddd964d11754d13c90ebfe4107b771143ce084c98ce0d00f321003d7fda95f83f8481a79e59d96128b2dc797d11660b50bb7050253ead32a99d9ddd4d21a841d173e0ea2edc3afe59cfd59a1a4f673336bbf41a1db48be09c9e2742459b7df654f7bffb2f02d705f561dd59e82897ce2476d225533fd8c5b5c45b7d255a2b1651fd2e8f259a5e7a6ca6f602f268c1a9a8e07e69ea11e573cd905fe47fe5aa5a786d1721c8188a20216d71561f3b73c80f49c4f28b7a28eb310f4800c02219cc1a3f008406a85d2a7249529e6b156691991211601e7e2a9ace5d440df72f95b906e8a50bd0fd0c1217b4a1c3863ffbca3b21ae3a33fb5f3f387f1e8afc826f953e3a40a6cb3f255745883ab116cac0f1b00ad174e6e4d37a39f02990eb1e85ab69e751d3741d0107955b0c81ba19f37e2aacb32dc3ebd50b3b27b54ad3920e8198792d8a5441a9fa4b17469092476c520ee0b8fd74ddcbb4e459d57d39fc4d06e0183c3ad5185153156942ebb512d734b1d0204ba79097e9bbd1e515a1397726134694926d6e664814d47fbedb38c192319c543776e83c7350defb0bb5b2fecb5b7abbfb9b066cddd830a832dfc2d0a0392c32c64965f3b4aae963b306ceef3d0da93b756e294b38ed825d7c01068a45230b831761339a903e79daa0a570c997e82d59e137a6599f94854e9bb200cce7211f76042213d8bfe8e009ce730da53549c52b5c220c472fcf056c6e1030a7664f40612b591de10589ab68ea1fe2e66ac3df2480258567ec6df33e50c0a23cfc940c7880bb65a85d404f882a985d5c88e27a9f757df6500d428a41f00b12175d504bc6c02621705f4423995675c78967b991fef1073ac7044a0a5987d2a34dc758917b5a3e4f4505d7c7f3252343ec6bfc790db747c1cb9cdfdcd4b379a2a07ac01adecd8e2add1cd1689729ea33af544536660064e41b9f452619c12955897b07e6832d8ce764d8b500d3db07ab24b7b7634c778b6e4a68dc0e2d2db0e8788bfa0931ab6cd3f73adf01865f76553b08fc04a70520d5f7df3e87710d29eba8c0e075965f5c1e17f5aea4abf9d858f19892fb8cc9aee53b016c9659272c7bf4728fecd9d7a1e541830c173ec8c651256271c20891143935c119906e7ef082d35b4199d479c57cf49f9feb4bdae6560c64e44c91945ceda6686dda6d134f338e8c153f304430a57a829df69c8239233a9d3bb609cafda3ee69d949553a725772899ab245c84dabd1d431bee252188668d73277b8b2cec8b4c3271ce3a2f95fe7e0bca90d5b1c65b684c462d4b15ed6439f9b448bda0ef81077ecd874f91d9460440232e4386446b8b86a2e8af65c8f6e801bc4e7b437a883c5e510899f12693af1c81958126c5365c4d82c9ed1abc59edbaed43d710d38f7fb6db8633c4f6c3cadfd20f23ae1827a21db899a2a28ef0976e1cb2797f395eceb6760c8611e62a75ffa4879d56b59e473dae2a63e1fb4d9649467b565235fe540656a89ffd115b26395273cbd8252c4e0267728522776ca94ac77646307c9aa52c583dc588b41e06458131a3e5a3887b0bb4a692d2dffaa6c6a6290cf9d12242aa8b3de0affe0a93d154a4a57298aa0d1cb1226bf4e01da8429b32574031a897051a1e4905425ac9725e403be7611a51518a51577c43a620a0c55192d797a66b92201d2c269d8c8efa86a0bbca813199d782c175f899710c0656c5f051fea2df4936bb912322aa8416e0b2049d0c5597504e4598f09baf5d5764427f5e857633240a194a2dbe8e947e1f4f09f90cecb28ec726d8f9f2a0ac52aa7730c9780aaf84b03a426b704f1d321b69d9c76db3e8a0b42868ffaeb699524e8366b35766c2f97c7272c81b903c72ce5661310901d02ab46338d5004c6d12c0adc23f220849f12b2a1cb376b7e478cbbbd88171007c4249ffada8f4d8db012fc3a0af61a09ee60492d997ce21a9969497f0269ca35aec36698899352444d156286a68baef6c0ed39818a4816899ddd92edcf596b123a3a51a62c77de5a4374fba98d1da6e382fc58cd4fb993e4c844d93ebaa36af13413e4e5353c71e5250ecf9b7d7a6c078c1fa5eac78049a84d93e92e1b1411e4a2d22ce4348ae67e3f01d3437abd53593a2d395c57862e5acf3ccfd07e18593abbcf6022a056a75d6dbd337fa51984387d2ce28b8db9074bb3c25b3b45e0800ececb1ca4204a476f6ff9c45ec7bc82c73973f19bb21a40aa4ce3d3278a23f7d8620da3a1c69c115dcdb24600d3c34c84f2e297079e5fed01067f4662a93578ab627f8b144264f426a8c2eb2c5b96497667ad99dc2af313843aadd790dd1ce8bbe0920515c59414eaa4814d6c48b8a423882161cd14e5eadfd683d617d7c2913ade294628d4948d5e57319b3fdd320337600e7e5af97f1c31368329fbaa163ba41b44c2c703031ff8d2ab993ed50fe2759d102031d51c6e45d6651abb7de5b1cc253d68166ec19c3859c19aa1eee5ffc1a8a77de11f1012daa3ce6e60a94357792c19f44a5e71ba9cb3a93b16732fd5aab3c33a1007436d3b47612bee18d8e2f83f8c4bf6cf64ebc27e39ddc1dbb490284e3575c6b762e86beac2ad2e064f05814d810d5741b0f21cc9c0b052154ac19c49d2c19a88a81384046be6ca8c3646e2c606edf974796299a3d94f22a3a662eaf079f39bd67485c281a8b56617d4751ad54f67fd00a1979d79119a54f0785d507e1ea816a1d27e9e0b1ed054251e0361be99fc06f03f40b268013ce60062eebd92d8648ff8089e497684e25b768eca934905699a57306cc347065a2e02bd62f6bf2bfa6d0dd812c08de3779134100d6b13a7e9697a99805665c379fcd002e71235ff9f69a366693bca648ffe898dbb89754c8fa13bba7c8cd1d7d4ccab3ff23194d3190c8f4f78dfdb0ffc6f9d3078625a05144a0962262823ee9a2b7f307a913766cf382e99e14f174068d6272fff623d110f0d012c3de6249555b649e9abc581a8699710886e26f8d34f525348e879f805f99f2b3fbb9ea598984df4d9ef4677d286d0b939b8686f87bd847dec8bfdd8935b9fb9b0643a98135964c6b877334869b169040a25a5d97baf859e972908480128cc2930b7585dd3b4aa7007cf0c9f8798679996f34dd791faea7356d02af337e6952a2721236efb0295926efd0d072cd9e8dcd0ac47a3101c4195d4dac508d939ec04ea35e759310d13d371c190047c1ba21c913fb862bbb0a3c2871277267428adf268eb40345e64dbfd6291a0ec87b5f1820e11d2dc5f74b28f2c68305def04ed514762f1fd25f3f0a63d5f16cb80817134d11c68b028ba7b2fb8084d10fc9cb4676177f4d356caa0a9de32fa1f18c6b445e18e820b4ab816bcbe54e07a6607efffbcee6e04778ab68ba1fe40c1bea45680556d134732a39fd0085e8d72e1ec88477ade504696e3c56f78ec68a701fac18e2692660623d31f27e59e5f26ace0ac47ed612a3adb87ba067e756c43fe31a839ccf88f430097264f9847656a803fdfc2fc761d18c1984b6873f6a8594c07b9be379ba68fcba287bf50206fae7fe82a045de1e0a17ac32d592d4959effb7667addf10483e6aae6bc7e796165cd61cb44ad1d96497d20015b31022e4b6602d672bc8a2b9dfc71cef8cfcfbff59995c57bcc3e1a2aeaef493826a9c7c0cb033875b769d70f1875061915a95fea51df549eba4c6fc9663eb3dda46b005431073bddfdf51c988fe2fea4ce96e647f398e70dc4be3d234c3d55aba9dffe6eb74a809c1ce7ba40a2d79a4303a27bd989a6922ae74224bb26a55700d62bc87188c87fd643f3179019665682d18dbff3cb385b0e56c2ab2fcadff74ec28e93ac6d93ed792ed58476e965effb3638d3ef2dfbbcdd6a25cdb027414c8243169e6af2a8eca3133216cda075214f6e63ff80d120224deee80229c613d43be868d18bce403dbe5bfc4c1bd6ac553aa74b8aa4e1f387ed2ee2472a57decb7f035c4bef48027f2a4adc1119b47d5a30d07750110dd16bdbefa371c78de3b00453bc80f7a030038df8c4502e289131445d6566e25ce5ae123ba5059001c4374e6ceb44ad9959405b0f10068d7237124be3a2c7c21fba88e9fd654c58e9b9b667765631a0bdcd25237a2b02127a1ecde3f8cb5ab4ff82a8d3eb512a522b0d5ce6275558745201dd924fb75bbdd6451b0894350da69a76ff634717335760770dc375951517f23973efc3f1780b024725bc604be9bdd767829498b8d8841b9993c624b7f0b0b6fbd0d9fc31358b89d4b4c3930eba00b0a09dca2f50ed9b3fcd5733a087cbc3fe0d0d8cc226c1d394e4a0a21fe20904779ff6ce31c063f42d525395f9dabfbb1e091aba5dea99f30b6c03382e118fa5f5de251a20b598fb4585292913de08c56ad656c5ff23c76f6ba364fa4d76bc73f44a1244ab72c72bbcfe99cb8a9a335dfaa42765913a8a1980f43ad888964dc000e1f37845ea60b239e064ca747d2604c1702bb9d8b8c03aab7273f667b4371086e6d79d446507fbc75453790375dd0b2c345e0226ad62b4cb44f1178140eb74b6bb720c5b6d62e2f6e57492fdfec02907c74e85ac6d029c87ba80d3ef43f251da8d6f0ab0e604de1341d13199e83c3531dd64d24507e21d10b74586eef09fa10fbadb7a9a2f83934ede5d500efa6c10e26746358c1b5ca9f9c093ef9196b6f4d9c62117792a38c792c32a7edc94164cc346d2a7c83d0e3f1f69b273e1ba97c2eedb3344da2ed5d47819b555f830129a484a6befe9f073cc12cf62d1a7e990c3b6513cdffd8e35a79837e252bc231bf1c41e4e42bf1f9591169af1a4dec654c413d0f22f10d5b534fb5ef8b2c2efe30646295b37358d2790475e48371691e76c38c9fd15698037a9fea3b4baab93ba65ad605275a997aac2b2daa4839ced32ed5fac94367fae1c8c4abaaff403a0c6fac079834a4e8c0225f11087098b246f88667ba91f7845f6fd66a2ae2c9f4bfc1a4f383d8a7a5fd70d90a3d76dc2e4f31c1dfc83dd78a13aae75c6828b0a189dd5cab5aae25dbd222d4cdd300962382f6a11905718d9ca9a72f15b512012fc71f5c780afe4538ed597c927d703f5c57a2be18b89d93ecfa67cded5401bb0409d2c1ba00ba3dbfdd333d47c99ae96de12e21dda93c4f1f982b78b7d0751e01d02a9c49606ccd9a917bb77b24b0d2e47e2a34df868a0cdaa31cc14d25c0cdec17d9c9c9c0c463dfb416148b40942bbb6efc30c9556edd864ac1dd1c9bb23b294b8ddbcf77ed030fd4905a832a950714df870a6baa6bdc2c6165986227bc2aa4c33b8bfda9954551da0743f544086a229944c10d64c6323969ca1e3eb9299e92dd313d3caed8c5a45e3e3eb27cf9497b4ffbc7c2e8df4f76d5d8b50ae9292d57755aeb263f3d3461b3d52ac715509f7a491d6a82d20ede79b750e8b4d92bd242a62ad371b6c13d826843f49d40fee2bb21591ec8bbb9eced28743fb49dd5298bbda332ecaa1fea310f96318846d6a17dfba2cb0ec675bd7e6ab1aac92f036672576b3eaefbed0a6c3329a90e08d62f0dfe6703c7432ac6a93350df0e73967476b63eb638e38387b4dde5f736ee6163a630c0f7d362c2e99438ca222424f33d89eec921214b9b28793874bb0a5e0f0b0aae431d7f5bf3a781e2237b7e2cc153beface825f657c3b810be620d1ed324209872fb696da4c491e4565caa7571921c554c00a435fbfd423650636689e90db201626528ab018019a6e644d6cbad770413a0c1a36ac19608190b161e93d0abfd561c47bb71c15007b478da7161778cfc063f2e72e32059ad12b2e026bf0e69bdb74a700a7980480e9e3729b3dfe7728d749b0794df1714d363557063a600259d008be6c56c713f1c98ddfe4870decc973be70e6561a653697454a9ba95868c337cd4ce037110619f3d5d9b26fb5e7670fa71d7320524c21184b2218c1fa6f01500d12c6c95e973f5a647dabdc91c820fcacc61236e4d89b73ade8cbee0bf135820e7807b93b591831eca54d0b58f123e5c06f34062debdbdf7ace0e1e07f17d79b72ac0bfceed4e084f937803df19ff0b9ecf6e12f63541f7c90c40ab35c8784f5912cdd079c4623ddfa814d29680c0588e565966ed1760b2c7e2f16670370395ad00e08add38917de042b000a930d334e80633b1bf08984c9edbbdf1fa3474a53fee77fae6201eb9c778fb597e00915c5867adfd6ab323b3303d2d98c6da26a02a7917dc35864de078eaa5d9453f13a4dc28d1ea39c72dc8d41b67de38a0243b06ecaca4429bb0e58c2108a18263541888afd7b161c8e9e541716c2f64d27649e8cfb40995373b5f0f877841f0ef20bf71acffd53c53f7d453faafe9e3f39d968aee08235eb0879e562ffc83f826341e577e41bcd769c6ca53d95935c2c6e600bb4b6d7b4a30622309e8e20b951f65eceeaa9137ebfe83f6f7f18e6ae6c44cc8121640b1240627e959198198d1691951058d14951009dd9d48ef3ff5c10018be38394a46c8ff7f3c2bff22aabec07bccf5a64a2240eab8dfee3aac1b32988d570e1fec627072b959f2e7f790a77d1f250f91f4d3c7be462b1ea614a6e32ce0a649f5ebe35813a874aba80b075b4bca680fbd52690032c2448e2642337bed565185e76fbf126c3316a8ad63e056060aeb992421ea3e2cc606ab351d2abba31fc659f1bea2bb2c203d7a5f4eac2ef7ef1b20033766c68b6070beee72434cd01d97bf55fe059aed2652194c4fa26992a9e3fd5be20d3ce84feece090f59aa47adf963caad571e4db7cb73d212f1f85df715ba86b3c6e3956aa742f87e91d8cee5341bf427a0304e9beef702ab39453452581df8c8e4a3df4de32e774890ff9795704516f95dbbff88a5b42adb75a7174f72b7fe999e72b89a08e938bba56e574ffa73742b33b07e25a780bc05b94fbe224e3b531fb84bda5157f478ad101a5c7205e5f41b82d0690ab33a8e57af408e826e7698241af65d9a64918a237214293c46843eee8396c1484770b7da6333d6af3ed63a2ae4b10b85ec98424f092d86f02c1c6f840de21e7f4626330bc6d26c2b93106134a7b6a61a75824c3c09bc8772c84875c24597e64718e77ac92f91f88209703ab1bb7e294c067cba79d7c10db279e57b3d25bf4b6d988eec14093c29c4272949b75933119866861183b2bf31316fa58256717cfc041a984aa48cb9090419f9c566e002f41dfb8c7b0740ed5efea99e960f4af6ac419cdd661faf019bc74b0a2a0f1da65e02e3a7c6b5a38e58faa085236b08b7b1d565f27ec9ee11a9a19f570851a50a6feaf59a9e2361f2eff134e92e70521bfdcc7bb358c236049f196c7758680f300f4403271e09bddfe810c33c88c96a27570658da5b13983f8e42713616e0dca22e2e26ddf835755b0f9fb5236551e61cac98db0e89b53be8716f326a7208c9d3ca8c52adb56ea2f1865302b859963c765345ed1ed68b18f3330f94a441a7841bf5a77941760177d632015284b0a46132ca0639b9b80a7a562f0f42f46d3477261a0713b4c1386dc549a819e3f4528f46c18a3356bbd127c9dcf8e3c92538bc2516a4a4e7dde9cd8bf5ecfc05417c1814de8a57061137287e25b70674ac2b0fbfc8a1c864e2340232021fede195c5c6104cc440c80645bd1f20fec44ba32f0dedba6240fb8e06d487ccb68c8f9a0aabddb115e3a310eada7f35058b4582a623526cc5a9224758e4f395b3a7cd3c375bd4514d2d3f611a5f5fc19566feefe89b4fa7f60a04e85ce26f9b1bf088a43d834af2ef6c9e6e42264948ab9b772271fcc9b112e1e9348736c6876e7a1e01f2cc14cefdb2e457d22ee2875e209aa1fc0e5e054f987ceac2c44f34d25889718223d4254ae901382ad2963de1ca7acce4d7066d986f539484f6e23b66df1b821ce97daf89e4c04cef9ebd01c7884415aa29a8589c19539223b7849759b2865f5a0726bef02843183c7bfa11fa6e8daee31fe3aa8dcec5fc719ea163eb1f26770b5f2264f6017492a389f145da37128a89a46a940e1a556f305741cbde0d0e60b74441736292c3b1d09f903bbed53af5d4f48e642d4011fb507f19f6a6101a0b7ac0cb7acc2b7ace640231c86a90889988ddadcf6a3256c7c3f346ae0199722cf98a663ad71bea41e034af357a305cadcd6c47cca52db5c0cb10d952cc0bbdeeb59b5e0ca10c06f2ebb79bde83631291bdeb5b354f1b511e5873f5efdb5d533d9f164dbb5e500109819b77e114ba677339f9cec9777166078af3bd16b6eb192505531a2c59d25333ae8c7f513f0202e217648dc088d2b71e045365b55704858b3b145d751ce3b34c7607cf85c3b7a5445497d50ece1852287a706b913427671cd972e5c40c1c4aaf035ad411279b99b01ca44a8f4cebd63c2cafe20f6d11b4363239c2a8cd2ebced887927d2647dea33805fac9755af3b1a4a24e64efba4d47f95b3a682cb8a15ae8a270e6b2b6f30b96db69d212489f09906b89a891a6878c5aa8b9d4c31c48ef05aaca7fb58d573d301d588035f33d887f7c17ed675fd41203ae3dda008d07a45bffd27ef96ee2bd2c987b285f0c348bdcc7282bce4aa2b2064b82db8e87bf2f838e8a05c42f21fd3a9485f5676bda212f59d2ef53e66878fcff8ab45f8d66197dcb8a35582cbc831ce490ab9d73fde7ddaba551a9b985266e3ad1e714c18fad3253306a85a1be18c8b1c8abe20f3b4b3cfdaaabd37c27b83411a49d89ae4e1e7fa4a481870eb846cba2aec329e636c7869714c0cda82484d444f23ee04edc67beb6f97857d9cb56f2d5ed3002e1e3bedd56c19f0cc873c1131106a8436515a8dfdea202a02e4a2924eb565244d69351c5cbebcc60e84c6a4cbec749b04790d27f2517ca242c5dcb0a7d33cf561a49e5bcce25ed34de3437ae848f3df1fda61f9fc37d8f7f307920eb3abf7e8990ccd62966ce597145e78dfb4addaf6385585b3fe0baaf763411d86ccbf4519bfa362ed84a5241deb03d64986c51a335b2a7baa0e1a4aebf256167789831e327db9af1e9954940a83847048343943e684955b8fd9c47bea0d3b0c73a892b0b0b4d100cc35c9179b897733d3a8cd861f88c29f521c38c6298047a6d78d81f2c77ac7ce609ab25667cd531adeed28351fe5fc2cee3adff24679d338ee129654b33a88e2e82959d25b174a4ae196f7c1b95e716d895c797d720a744077037546468d66d234cdc6ffa781e26c9560031580052e08ac54eee067dde4f8599a171b50da88f9d85536b723cd271a3353152eddaadfbe2effe7bca9d5fbb6cf9e942473fd81a76cdcf6ea5a647ba765e1f4fbd573d4795e9e5034da2bb29462bce547f21b8bd075def67bdc8552b9c0bc707184c7ccd36e2cf5322f677c302be04a565156069b45fbb491d3e608f0b5055f1b349a6ff0a6c26c087c58f6e357451c78f732273b6eee380041e23681d5105037b76d1f7e2cdc5ebfe038dc07b7131ca24ce2defe8035e7b8f3c4633b0ec148221859fc474be99aa2114d41b164277a26a084b2d2c3c8127cb397dcd2b812b8f4d97879f25861cb93f7e394f651c4430f1a6ca511e7453e9d81da7ea06b1724fcf55c46b6d8c45d38c48e6c8477eb9d9aab76712b05e254f01f5dc6532d5fdab16b8aefcfcb9c98c5e86005483106a92183c325e22923bc51ea487583df7388656c7ea3ac8b752c3c599834edeececd61a5db7b012efffd0dd9ed3ebfab88ca1d250fbf648beaf7124dc7aa00f1238e8a05e9f7037383fd75cedbfe5bab024fdc305c0e81aa69a02c08c0bf6abcc9e14ee55bfe435891b334dc19384ca40aaa96e64477bf0f5b7df5b5c1575f6ea3b6b2afabda6061ac2be3aafd397b95e3d25dfc8c387fc38ba165d36e984f14c60a520e1154bb5711c170b8b208726b5da189e214c1a870288466e48350f4ead585bef8b468aadd0011e9c6e42c3796245654bd6b3003b4a1eae28fa34d4db9fc830fc6b30441e0e4315f3b5238f3f53f5858f4990a0bd4af41267bcc2dfbf6f469f30a1595bc54c8e85e152b1e26b7fa3ca3ca9e6a5d61c8d022f5647e9f7143f126a5fd62c192cae09cbe4e3f9fdb0875e0cbbfa8082ff4f9656996bea33f6e6d7ad8a4787cb1e4b7e02349f6965cdbf78133595e6abc596aa05e934cadd27b75bd780425a2c30ed304534ae15dd7afbdb3c74a806d1189fe6f8c027ad5a37c3cf5f9f7b8eecd957cffdcb3f0d9c2a8ff99e2af48e22ea10b8f0be2416390f8b0dcb58cb2a5d968dde6906bb4a38435d8cb96be27ffb3ee0cedbcdec6674288993b34c8e589c09d6844cf4e8eb5cd321c738f946c3c89e7bc5729eea51a4ca03219aa8fbe85d17ce02d2699ac06a1b2dd5fef0a84af6ba1f2e830278e4688546f28126d2d8bc96727e7c6417e3b2508de3b803607bf600d335c0b2716b12e5f2113928a36411256030336699da6351bffc6cc7562965e688fb47b598d6dcdbcdbdbed11a58135ca7789d1771882c21f869e369e18108f42f45bb22df8280e9a446071dc42375e792d608f74df4f2f58fa9f7eb328cf3bead95f409d9bcde4413d200104c1d978b01f4737fdfae2b3737d7e42634470a1b33457acfd73237753d3843179d02b49d0301670a9128671b7c8f3a63462d464db069cd90c5753f099ddc0cd34f7a78a9223e3dfb7ccc41d006f91f0583c9dc5687238860b4ec288e57ab3e7d053d585418f7b438c06867a8eadef33acdf190a9686d3e0a1f7daed3c20cd1f23ddaf3e5fe66c7d668beab7542a483a2b02fe305471ec623d49d3e593c62d8de228a61eb7ca26162d0caca2ff5642b6e7d43e2c6a2f0eca7f1e75c8406609c2cf362907754cc26fde6a12b5e2537e833d9e1f62ce18f083afe87d9d9cbdab9663366c176e8a1baaf69871e6ed7af54001ae4aa302dd03bedda346b4d4d4822dddf9d37e852f059e51f60a4ce99588884e9698d1f47f84ef3a7d14be6d035413d397657aa3883e2c529ed64aeef89b9d58bf739cdc6d69a2cf1988b352025c3500a4a277addd7de9a27bbac32150dcd2a4ca5816302daf524e0168f5539ef8e3bdcdadddba7cedaef20e7b70efeb11a0007b50d44eed336478835f6f7dab96cf7ab6694dd3f748d81925f2652c20fc35e397217d4b68f83b601928519f7932b4efaeca1b0aaaeff3e5212051b4e90ce37b19ae4496f92fe65c25b0f9a27ed717ee7453e43a890603cad2acac6a84801def286d28828e8fcae1289426c1fea130a865aa3d1b1e02c1e93467d1f066013fca552a999b18c0db2efbd41ed12a98157f3532fc698b79bd2836b88419093331b6c1f61cfc962f2445d7c33968b193f041d91a24f9fab16743373fa15074bd466d945709160fd39efa6d564e5a8f6890dc336bc17ef70107f6cb538843f6f0174d54809fa8ae0e9764101f093a930ef9b3bca85a3848589bc66a0060cf0c63b3de14c5ca9e7bc9b2a997c9207aaa88c2e64412239a7e076f13abcac092eab07ac5057df7ad1d818ed76fcb0c235295d22237f7c79faafcad7b71854b55b8bf3ddae524900e42ae64f1ed92cf60112fd2c7e1eee8be3694198520b45379f9117823902fc906fa8612ace1d3b8f8cb4f9d4e2e75343ecc92a110d1f5e68696278166898dcb2b0b22a8a67fe2580bdcfe9470b2c069972cd07bfb4ef18ff8916bb62358f95b99ed7c410fc534c2841ef4482cecd52ce3aa00f8a419f5c1d28f348db9e89ef694199c20bd931d77d1e7ed6e339b4e11c43d8db4194ba687db9fa9efccde437c971553835f944a792ad78a7af6708af744cc3f83512e148171b2f6c1b896e125d7a47f4d92922bfa02aa118c3cb609fe3af7a8816fa75c6909afacfe5f4f305a81298b3acc11864f050a661266c9582cfc28bff9d55861cbf18caf980b7892a364194fadfc3ce7c5c84523828d9520a7b976a59294aa61281e28e3f52c31ee12b8b628c0b1a7315e6e87fa5cdd2993e789c324f42749ef1f100839159505a89fe06cb5229512839668e4ee24256b7c69a8e156655a74d7694493b359d6cc262f1d9a3ac6049ab528b2e344ccff361771b143f7f47766a39378f2f920eb1696c438492d7d44c4b48e0650189f4e79b676360aa11b6a096bb7f0ad22c6fa122ce627afb7180c1c23057be21c390227caf955e10977fd352ae64b7284a15652c295d44d7ecd7897895afbd823531a2cd7105efa77193f6b58c7c1dadac2c5099740aca870aade76248740c32b42bd30aa72428b316d327fb4db9223072ac8dd04590ecad2eaefe66de065ebbd287b3c7d63d271837831112bb9059b7f9fa9add3a2f9ad13eebe08897480ed30a35694aa074dd04a84832a8c60077e0e8a08512b60b6c865f1c43bcdabf355388c1b9471c5c2e23444740fb5e3bee70d3ddcfd1968e8995592ace22551422e65ad6e77ab56b4317fbf47327fdcc1a4579ad83d2a8deaa37c283fbb3ed34930eb3d571fc193332a94c223005e551d87af318e9e5533f66ad60a5f304aee328bb88fb1d969b0a0d6f1673e25811632fbafc3b6c2c4d6d8ce49893a51ca22ee5eb9c12dc13fd62b013eb0873a1c4b7ce330bc719207f50bbc4abde79c2d191cd6ce4a6f28c76a64cce35deb7dc582aed97ae59890d2bd7fb2168b0440b44cfd89fc7b2e507d96cf09ef9bac406d43991027a2250a6e3aab639c53384c1b2dbe6c3b766317c5ebf2fe1ac334f4c95e8316fc73b11c367713b5e4a53bfb8dc913152be589bf9587be56b32d5ed32fe959106edb004dd20eadeccb43cd98c7ec4eafd593bf91a8ec11dfee83c9d5fb744a18fdab90bebf5e4f0b4ee5aab38d7ca2f77f525354fc5259793137cb58723d8cc9fdbf0b1a2f57d7aa9860bb725dcf4b605bb75855b23d6e76ff6f982ec682665fbfe59389d16a3c72b87d1c95d3974c3f1610d3c547170f8e060b8f97266e90c185cb853db82b141299a9c81d022cea450ce321b06daee274ae4fd7dc41ba12537dd121e3d7d9d660ab16c24a4cb0a8e7af58c0da656938ea0eea982556ef1e1a04a24451581759c08f52e971fd6776a76b65945b7eaa3734e887837f8ab2573b04819a3e3709390f8d6c721cfddd89f7c84885cb6a5389e199321d7acf0e7be727090363c9c770e0844cd8c860d30d7644d680f1254d9dcfeba6b59f8d34f6645db6bf4b42b2c28d10561c665cf93754928ff1b77c78ebb26f440b882675d3f31c3d25b496f4274da9329836a1316c80006abdc62bfdfb0ebeea480767f2aedd7224dee69533bfeeb70dfc4c115d05a5a86adac9f7fc92e3c9c1f6e6bda5765fe881d949e127921a6defad9eb95ea50aefd48d4551af18c2ee7f8410e5fa20d3bc71da1002fdeeedede0dc449fdf4a41fc8fd1e959a0db91cf51a6f85a8f04c20e98729035adadd746191668e8dfe60595239010d210f04df4cd8f56629ed1df29b815964980724189af89d059fc55581b232eaaa02c2ae51cb8091a8854530fd2ccae579ce2632616712597474594b131427c31413be4ecf4e1a9905d6b15c1e45648051fce338008f188d82cabc2edbd98e552a57cd7b47ceaa2644b7bc5928c240cb63a27ce304ef8024a565ba3a1073f9dd4e060ab54e89d9d9b0810b87c6a4fb5221f2a17e7208a9de4bdcc2eb82d1fc1dad90f5d1bf773312e9a54ff342f815fd81ff69efd296fcfc109c2f1ca574d63e6072e4cd076de31fa241446eb7c4a813446c20ed0cd0e060fca3e191c8c630d612f615c90789d31c1bd6acc50b4aaa21c8c9c5820280aa5a7d23909bd4d63fb7952c8b4a798c178f5fa35a117ad46afacb4fae3164c4a386b5b15552158320aaf017f953a24a3659b7cc3ba019d35aec55b46aae18218c8ceebb2440a8ea77407941b082978ad6ab1f0a72848de58b20145214576fc47b8ce2894622477a83daf3288d6810544d5dc486fbbf89d10f3c0cbb8e4a09f90206f25ab723544ee2a88dd89b85249e049fb51ea363aae5a1c4d3fdaad57113f03b0df1b958882c284f16af387a00dd64a56ae544eb6248b10010bd899a68be5d8d87bb40d6ff7befc0c428600b2a53debeeca2dcd7c8c46cc7b2b63dff2857cd24c09eb58b9228c2d8fa640d5811b19283fe03d8dbb67155a3b0f9d8f9a8a17fbae78cee7f59af7be100b342c7c8b397ab9b266ee8d99349055fdc93f83db0d22588af0e8172878a405a1a270f2b223b2043110363adacc14df48e3e10789834bd45498ce5fa9efa7df07abc07aa8620d42f63b77baa1b58c2d1af9c8e157f33b5a2ad34ba826962faddf31cef846913f43d0046c12b9a5cb1a9dfc609e3dcedd5da31d4e12852a13238b560df54cace53d8e7d04a79a0f69cf6fd0ea3e91588a37d26ea1e4dd2fc0d8c41a93d873473b7d7670ae8a6e82c6ce32716f66b2d0fa066a1667f393c4e6377b2cda4b09f4ba019a0d44bdb958cf9f456081839428f15176d5040b2b3a066bec3ad60dabb26ec211edac477584d0f2b6b091b0ef050c8e1362b62cec64c8b7c6429755340e3512b771198df9dddba3c6a1da0ce4acc6f7da823b33ba44b8a000e47410a95c04ade522b32ac99c25c4c191f6ab425564812d22fec108ea7e0a1b0befeceb3b75359a644bd053c84b6c6980d619866f8d66c6d0863bff75b346665f5675b69cff71dab6ea14358394d306f970fcd9a14e1a094a8e37a49ca14783b8b2dd46851431c5ff117026f1f2677f307b9bfb98e056a6632bea531e5e0fc05bcd659f2c3ea161e18faf67a8f8031d2cffc29ec3647cf9a6da5b0413d0a2815c6dc2ba0afbb7b0ec29e394f0defec68dad06d03b4a0911b116ff354b97a40b4df1935038d58db8ad2df1bc709d535a990babed801a7e631cfb4a7fe21d441a9a65debd1d24cd461b757fa20c12df300a5631c42e2569d0746066d7f7277e7f18ba50afc362eaac8d2a83534c037b658ed2a47b9ea4f63d2f10f823869e7bbb82331b3847e409bb051c69eefee18f5998402ae0497e0bec4baf4c3d0421f457b32bdad6c82415c4f5d9d000328908c0365c28aa29e5da91e222767222ef78d6a245e03017f0e23a8d987b52f6338dd457241cf4a528362f7b7ac7c19fe8483fc9a94fb9a58a65d78920418a04192da020f20776f052a376e9e009731d6530d4c8ffe892df1cfb1a1d4f85a8339a7350623ff051483c8bb725d35b0a71789401df2d530dab32740bd59078c426d722d71d4152a69c7e303abc622054e70b4637c4359703658dc0eb600f37fae3a7cfa484bb7677105be56bb5a4228c828a61d7bbb18d6bd52deb4003279ac0c23b4c1ec475d5b27fef77e4e28354451806197e3b273ca190a58adabcf3be32358243d64aeb332e777058aeebd026a1ef73630c9a64efca49834571e6fdf017613d1c60c224232cdb9c7d98feec82d3df1db017300fecd9d39bfd2ad0d02f6ba5137d42f082f444d977d0f3d47183c172fad296d461b5d8493f3382b06340c63097268b503293eaf02eabf774c408f9c8c981e4c069c1930722f50fc79589d843debac7d4dd185c159228734df9fab39739a143d3484a80316c36cea6b839466a79edd71ce64e6ab4506d246d0d77dbbe8d04f861d51d69c840ca1afe8d947cf0d70444045c2fb4014d50dbce735d7d4daaba069f072543de77d61a8265a469e43efebd123b2fd730dabcabe1dcd1574758cc95d662bf8ee213ee3a44d86c5df0a8a97d3b79084bc5846616995182ca76092c0dc438d358d4954b8ce5dbe57514ec703405c5e1a2db0591fe396a713ee4cdd64c298f17d0b9a5b8a86231f163b83a567d9dbe8bdde97e5805971c6696526147fcc269a41795c338e2a817818224befe52fc1f0109adfbae4cf6bd5f918badfd05e6dab2b874ab6f64a6c8ff303d213de10fcdb19f5defc86f2e909abfb9f82bb4f75a3f9660b269dccff7b4e907cd8b88c578ffac622443e7e7096e048596ca4767d6c14030d08f93909cfec81d631d508b87e632043ea857b7123fb93cd8ff3a5fad2e4afe443b4287d05769c88f399ace8c09c282884bf80d3db5bc707110291d4fa4d154bd368e47945414aa3150f25f9e2471c059d6bfa856319c548c18d1e257ed3ba1d43a103c020177aa046a286364b23396a9890f7188f0d2e03dde133c6fe23af89d53941e2fa51bf271c5652ef3c13e2cbc212866fc0ee467e420a50fc5dd46c7cf0ec7b5d2f10e962f1602b2f23380c19b2f181fb2819ec9d37fc4fc7559ca626461b5902c5d9bd7fe2412d8ee30a4c8a3caedca0e1a1422d14a0b1855b1d4e171050074b85e1e9aa3c2aaf8e467eb1ac60548bbcad996d9d1b24d159869d30dac701395288101c5a7deb44423dc2e79d58f9f80693466ac303460ab46bda8ca82f1a9a1b45b7ef3ba61eb8daf9a73abd57abcb6840eb699c5edb901b378724c4962ee13dff36321960eb543f95d46a0fe7c20c03a1272d6bc9fa89efe6fb7301d3ccffcd192063e7cfe23b697db20d548d431a33733aea471f249e5a690b701532366a33fef0459f64374edc128109019e3d8ccb9c43a6b672e086eb06a99aab0724f92d41900cc80209faab3a6d9e0f81bc786a74b79adfa3c1bf57f8ee9475df7530be69ad4298d5a488060d82b5e4996cfb460d4089af5af3baf76ea6e7242f3b3c863786b4c2770aac339c95957443b4cfba9fa509e43fad9c31a30942bc977701abc2fe172ce496751dcdd73eabe53b6e2f71cf31c263a092823315da4df26b5d413e7865e129a54deb7adcdca177f9f61512b4bb77e82699d1d743435ca0aecf2e6747e45ce764030e550c30125fae3f74f76f73a790d080bd1d3ca50e2ea68a4347d51998ec00172c0d55dcaf07746725007bd306cd1ac278f46277aba1ccf53589231efd1af98f2558202dcde1f959ee33a572caa8e5a148d5f787ce45d3485838b3cd572e58cdb15b6f26ced668c2af846e995425deba765148f9aa59a4488e0ebbead9fddff0ee1274748b1d1ddf77fd38d5c3b9a12096d8615855621653d6532c3556b467e11825f7cb6196c564b31691b556680f89c33613560556f47d2d5e998fdf16b9fd8225df6fd927f3ec4e811f3470bb642f8632d93b08c18df8a8988713e02fd71d71f9fe681d530e495da1bc2cb809a2f512d96b210b19be51973a53de54ade4b8f69df424992c440fd1f4b03d1b2352ff3806898190567057707dc8b83854465cc15e837cb1de3ac5af3f0ded81776a71fd5d167a6a6e7d8c3e1228185585c867c328e0dc70a1f2f162f7ea4ddad435f506b0bd6dc6a6ead9234d7cf916d7335ab799c949ec5ed04bc7a99e676d1498865887a188aaf7ecf1c246b0ef19f86f01b9dcfb2f16c46751f8c38cae3d154b19232f0e431972758b61b8aad4b7606c600f59ba072cc1069a06c737bc04108ac34a40a150717c60f4304122a2f1dd8df1fa275e661399c8a110c6dbb9347f8c666af5a83a8b151c1fd72662a5db15e600d05edb1f91c481a5e0c3970b6d43291e5e81af459ff88f25bd10fb983e7a0433f2dc77e6af282519ddac265a79ab1a8cf586c15678856accfa173153866e6604a415ac6b7fdbecde9827c627e4fc055ca1b40424ab3058a5fb6535696bd73be370863d81c90bd466b74940b68472aec4b31d109659756b23bf140aafff8499fbbb9805dfc2ac2c0d9cb0b3202d2193f6a89671dfefb50718a1d6978c4a10b017447881ff466a007bda8befdb4de99b38be2a86705383441b92533d0957cbdd55b3f7dee56db52d3b0c3d9e137ccc1c4ae35b94e3941fd25481163e0f96590bde3f6061dffe485c43282d70b3f115de9112f0e40d36f97fe4522c88258aef7f17e10b70487b16ae11cb9e3ce294b4145fc09244cdb446fbf27a5c30f2b866387575e148ceb32adb0f81c2b470c3d4a00a61608aacbdebafa142be7b1387ef0804b86309dbdd19942a1c3b1e998b11dbe180090fe5b95bade425011579793380403468943834392cb54f940433b951e75168574aedbb9fe3b78b2a6de1e5b77fd22da045d9271506c347e104d47dba974a0b06b83370698120b9cc3e3a0ecbcd8055d3bacdc1b47a3f663048ee24066d68810e620ae2bee932069fa3e9110fb6d24f02baef0931e993703afef54dda06986d711fec0b19ed8529272d3e48a18ba9c396abd0328fbcdf4f282b8cf3ee4763a70801dce5aea72551652e9b8fbb4e4d07d3ab4e698b9427321e41411862b6f462960a13008da2f3605e2b2078d525a31b803aae35d16db5fd4f8f0527e754a69f5787232d3d6b3baecb91d52cc0afdf37198dd52d109aee1a7b1108298c04a79811b8e5acabde6b4384dd2131bddf497fd75575d9ea29030fdb01e62f246ffcb1dd5e04e07926b38fc4b95ae6d6d622783bffa646bbe0e1629daa36f8d945d9c167a7c0c05f009826f8c8a1980cdba49130626a793b28df95eca2a9a20f33e77650c132114e0f35119808df45d8917a0fe91da32ec17d871e5377ce580c801dca295de619330a0985e315af3a06a49377e2db0b1f3925d0592454d620bbc22eaa4e383eccc13957cff4ca940d6f63bdcc2e98367b0c554546adaf79c8b1953c59e4ce27fe39b009651a94c054a2ae04d21ccf70a1ff236240b6037594b844bb62310ce4973844360af3320a19d2949f1793812ad37088b1452d559140a06d2563e42c0986d9f4423441f1d7a90d8cf664234d1cf0ea58218503359009807df7a6a8787df38bb30fbb7b46beb14d930ccce879bdba436edd392cd3843d8cd6bf223056bf5db1271a7b8b700e893fde187abc131f5797dc28bb71dab5ab1dd4183c5abda74b56d823be322c548826ece3353948e3eea50fdd2078eb71879378e2ad714ae08ca0609cfe6f317e8bbe9e11b252a8f36d33593acbe5a1a82fa35f4ce8c48ca9c4967bf100c2780c35a1f6e7a82e508ee760f76131525911ef7c7bf83ea25ca17578ee2482d7a9b2bae7ceb712586fb5045cd52c8c1e7ef9bc7f7245d9dd433f630fadedb8e9e7fbea4eb471f386e3ba58af4b1248f55296f44262d8f7d51e736c7352848f4a6303551489f3bd42b65ec11a46008caa8df727703892a16ebf15a5c925e39a048222a7edfa08b352b7232e8251e42894ed422ca5292482b3bfe590b68293d9b72de3ee3bda04636b54afff4a4ecf0b74f1459d9a6cdddb5f58b22f9241e866922dc1fa650b2d2805fbf44440822c911a6beaecfcd71985fda52af5eb8df4f43046090a538c272df54d568ead283664dcc0b5dc76054c13f82ef12deab45b1ce7127ee1d5248e0ca0fa7f7f84215938755c52d2ce3aa138194fe009c31c5d3bb8e352fc1fd2f03f6f0fd16f2e13e1194e26413acc57dda293678b4ad40fd3ba7f8831828383c699440e143a52ba11f68eba68aade52d6f4935c2b2b410d2793716817c00f052ccade5b5839919cfdaa6eeafaf894c1cb5cb1dfa063cc6f2014d820b32c8027d16edbd861d7db04b9a89cded235743a5d0809864bef02706ada8cb2234464702cf06e1273629e0107183a401aafd1e01fc06a8845cc9e4785ab40316c3d6d89c6f0b232a7fbf6254b2d841c8ed688fb0d0d0103633928bd5bde4d34498e59a35ae2f307c9ec2803fad70483674120decab2566b7bdcfa8ebe7fa47590b6b6cd5da3967394772cd6078c91b5e1809a4fc3155baff0ddb4679dcf226f3c062aeccc7164857cadcefb91e36d1b17bbf2883ced11cf8c0654b455565d61533e828625e50aa9b7e89c461f056edd7874e446dfdb6e872446726769363bde58a3a33f9cc2b76d6204e2e061af76cb04a40a8369801f7f716f8c87cb1ee005f68e604b1027b5d2ea13535c0908f29b96c42b5dcead850be5cc38cb4d22a18eb7dae71745c59b3c1358b3fd75d9920a71154f769bd240832175e0c3e429f6e537022e6a5ce02cc7fe94d4b4b1152e429a04afd6abb19244e09dbbc3968f937ea0b7f1ecafab114467af154e327a916c638eb208ca86f1be00d56c86d80139690685b7223e167f99d943447b05eeff6c6f89cd50266c9e90b3b961c938f8f1100081be8f05c1486dae74ad615a9f47866c17a47fa046d1d75b5abf637d2627302865f96e55e0c71f6b03b1d03b5098403ba25cfee458e00ee66d3ea3b2085701fc0e9b8587be764ab52a31a0e16b96743a3d49533621d56f8f928845ab670fc772b2495fd9525bb3e54332de5868d01171bfe29167303b2e1c6dfeeaafe80f9f19c6f229d3e428ebf1918932950dce72441f5d995ef4dd45f37db459284acffb25fbddcab993bc89718e1fce3387630b4fe080074c44cd19b136195050ac5f35ea18bfb2717274c108d273dbd107d75937ef9ced1d787d3d5d58a1f74c63704e050043a5c335b55c9158c51195a232f803284fdc02bbdb814411a9d557850e544d5dfc6ddb9c5e40f4df7a9d5fc9409667652e23be20164afc20802b2932860b79d251d8b1896b2eb8c4471debb369b6454698761edfd321a1ba0afbd3a426847ddf814bc544c46729ac05b5ea05b1548ab3cb7965beb921cc183c254180f12c9a5ede1f90a3993984a3c646eb7a97ab1c6d11ea7d1c7b9b24aee6cbac8475704bd6850f0815d331a2e34bf513cd4631558eafbd4f2410dd1b16c73b6f8ad3aa26078c95b22b7f76c22fbd4176954d3d2481cbb1c7841f360d83c22816e8156bdedfd6a29255d90fe3eaac4f411127ceed31e3b9a731277c8a92299cf2209652571f7e458ec0cc5df6555fa50b0f8ad90b6dc4e3a01d5b2694cf17ab33184c216ce199ad808162665ae570bded62262afc12323f30c3b86336f03b230f2bcc6f56e4311556a0a6afdfa60216e25ce42b36bb9aa48b36e67c21ed780b71029d3f71c7ac6a2eef4009605ab4b8245345cc5d669f597673b9a76509bb28d5e0e04859ea219978979055c700d73bfcc89d36aadf5cc16c65847e654efeead1711d124cbc6329860eea9d58da4ab607e01f75e5eab2163bb31b0b6eea7a02c14cb76f1cde817985a18eba34d34a8088109269995d5d0f6c1f9d248cc9ace53d4378d9aafe6b9079abe901910cd9f11d5af4900dfdc9ae2a7ff6898f22956240226dc3f071b26efe34354b4ccec534293453c2c0f537609872ba2661f6ff816c22c604932fc500a8640c53e53c82d34d262d0a16b5584a04dfb43c92a58879aa7f1cfac2cb7954876ea5f67718592cc5f200d5770c5e8a1cb28979cb2d88c08d53855dda7a4d460af80fbe4937c6e3b8e724dae130643a403fd4512f4d7518f060d29b27d964fa9f01672dec4e6981d9055f226e820f5a32a89095e46f287a921011cacc398d7fbee4a73ff6872a6337e15c0d414790e6e8d33bb578af922c5a338cce54df4e794932c59318f19fa83cf0906f6925c026e9ee0a398cb61e7436a69e4cfc26151f344aa2bebd0489310d35321e9ca2eb1ea31e4fb53270e034235f0295ca6ded054a5ed35d9a70d35e6cf9e988aafa452f06833f834244e34384e8bc4222df9b9e8db88f7a8db6ab126bad4da9c4ed3e27ad5bf9c5dc3711ad8addacff10cd4af3fd036887c959a808185f59bb42d2586fb8f27ad55d79dd76c4ab7bf68512827bcf735565731064ec9345675df4bc0287048cc7b53917e35b0d7bd2695c96c5660679a8a42c5c20c63ffe7efba1d2445a3d43243020205b3936fc8147f7a1951e68eccdad3b56e675bcc37c9e6b9f746106fea57fd9d487ba326e9a7c4a159f6d1458e28e4bf801727356cf17e8f62d331d0216194005b522d9ad035269545c1ecae720121e7e2e947ccb7c7acc6fe4c2d184a88e91b5a37d4e21be85c124ffe221dbeedaf4dbdfaedb2a9241c8f30eff4399654d262b3a29809e390d039e26b3a594317e09920b1840c823d133c0ccc3be928e8ed36adf883c0e54554598074adb9e59d13addef534fe4161b4df470733c98c05fbbfd4f58cd17a74fc2dd53647e048f68a4b5eb0f897dd6413056cebe74431296c4c1c40f2ccbf01a2553f11d85414a51af408fb66b97963469cb1e6df0d7e98f607bb99afafa242ffe95b24149c4aa68ff9fae2a3b3cefc3a32720742a07c5a5c9d6f397586b15a6010efea23beab9b20fb570645de0490fda6dff4e2dc1376616f7dc5bb9f55c3da6e020848807eb6cb30b6dcab15c26ae34d71e468b7762c7952589220a7fb48d84e1299044d0ffc8a9b1e0c16dec87abfaa011ac2b7dbda04d89ae5f42bc509e9be7b9b825405909904038bfea31771f9d4ca065dc2f51306915b1218cff351f9c2125ff8583126d817bddabea7ebf0063a659d83cf895484ea902297949eb2b3c932c81921abf0b3a21e1f81e1f62488f4bde70a39237b5ce71ee7c613de5bfe1c8f778063159ada0f4581441f48aacbe4a1b1018f2fa088bcee37631c527554bb37d8fffaaa3cd2f68a795d56d4b97c6d82e9cd6d5c3fc5cffb90c93178906a37aaa442f3d9c826c60e4429d644e1536120b44e78dd051b33f4c81a8dac1fbc03a4360c8251e17348383a7447e478352abeeacd2645df41d216aea2d17f7d7de6733fe9b0b74db9e209c728ebb90937b4cd6777f3be706691434519df6681fe40ab57597314b76f1d92a9af6ec11bc146c55142a517fb81e584279afe1f82c4b9f6f59de42d8536c974cc2cb5fbaf6efb18104b82f6e764d4dd9dd326f9399a8eb47a851e29aca32dd82ec7b8d63ad79665d83c54ce4a29418f3fbe52325b60622ac4380f9b1bb446bf270228e2cbc23bd272535349897ba4c225754827e96f391113db2ba40c5f218f38fc8508783c15edbe59fd02f8d390c81f71f35049b98562eda6b07f6ae8b40aa1b179d6826dc60c80329008ea428429db15aab7341d0d77d7f1bb95001eed105ea48bfbdb819de7c123e0a8c7c9c0f3756038c830b6f57edb9f08db4b5350e7fdaa67963ba310a698ea68b8a70c0b187ad44283310ef0f04f47cc9a7bc5aba854b0f12cb9d3e946d319a351945ef0bcbd934e8c93b3bb118fa7120906021e69ec54ee798d38a06a2efc6548c6b6e9e20f19d080a0eed43bd7911b12a1fe792ee8925a82e1c142fc27175b21c36f6e607b2a90fa12884661c8f597ac08a4055cf1a0ee023f8e96ed156d6c75e63b6b3e4a7cf3be628e0680eb36c6f3200378fcfb067d2191069a26cdaa9d727022dc060164e0f2abab8882cd7cd42365ccee6ad157e202044a502f3882ce41678a1f23d62cb1bd208e5379bd4a4b29503788562155bb28bccb30ef6db2ebb2a54fa5e814a4782724ae0cceac59bb5346805f752d943818b0e16f66bd5c94a6d9d9cf7440a7c1218dcd9cb8d4176556f2d6b70cbacc977a4b906fcc93f3721d3ab449957562ebf522de623c23336f9ecd37bb5fb20a3d2168960e5811d9a21c47a1cee445b6c33044474b65814954ba007ddaa599cbb45e5a23ca717aadc9a20ff7c9081b1c78c6fb31efbf59510c96b7a674f6277884ddf98a9ac5a1d903a604d7fb5559137cd6ae6992370717b065101729be31b71cf21ae4567ae0b904a94ea29dc03aecee5823436f9f5e12cfa2904b57644f917aa8d3fdd04e3caea8840f40b0d404df2432d574e9318bc8097900ccc4e4564345a69d99e190f8a5ab166cf107d56d2b8a62730c85108bb037d29ae52d79d782e53458db57aee3f95b7b077372f79493010487b5ed507a1e34b797aacba742d763eb2d28fc6b3ab0e465a3d90998431c79ddd6c20a0c0e5ebc8473eb09f292afb61b5adbc5a940b66b02642431272ee1f2c413a2a74c5832f5535f31e38a23e09933c073bf6a98477bf934168ea9316e8098ffca4665d34a84f82f96a39ccda930527965fdec5a93e51703ed86e6939c25f98be02c65ef6ed15a952481460b6d42fb58fe5d0b4a463b0d0682d8bea479addede703d8a386bd5d4a8574746d70df56da9b5698712452f865a63a1c13c40952e21e4824124ed023ac84388019d43561e1a8b87b90939e3b41c3d8484662ed62f10c133e2e9001fd9e3a6564520bbea9becefc1109aa80b84961334d966a5e908eacbc7fbb7ca5533560e5785afedd6007ce19e42c3ecb5bd5a7f704c690c8173af8c0bddfeadcce769a66d751ce175e648fbf203cab3e27fda8c0b1a5d3f2e322907da9b2791838fe2ceb8e3db4c012e02e9d36cf597cea62e06ac92ad9d6e763f500d757e89d8d32d1cc71484d53aad02cd108541e229d3d09315064d715109aef56f30ccd19c74a9da52a27aa8099c28970d9585f1196922b18d8d73636d70f38d4b28dea185acda050e95c1f0e10ccc960baaea802e2e95879ac4a3eeb9e22704296beb7512416d71fbf2b799b90cc3881d9ad056ea89bdb2ce720236d263dc3808fef4e52e6986912ca3d35809d7fba54909a35f8ee1946d578205237971fac16cc33fd31a6aacdafe8689ab1537e0d2c8c6255b68c0e088fe0e1b0f6851a0950b45e24b7a87d3efd5bbece158583812114a2ce22aae3be3a357993899e971bc20bbd136031de8ccffbdcfa6bb016270c5558896fd5cd2a0f71d1a06f5f8c69256ca53a60769bbef45ae1d4106388011604505cf82621ab691ab826dea4411ff57995eaf22adbab316dd9ed4f06ea5756816be7dc784d959e9bf23594abf8908f8ace78d047c5330778732cc82adace5f55d231fbe0826e46714b8cc3945f37c1c6fda9292c7f28002704032d07b641018c20bb56cc59782ec495124003c3c0088d9269bcc9312f37b9cbcf137b528f0c32edc5ebfad48ed4798b9942886181157e20e8eaf4b20009ef368226ed4febd360d3a44cb4563f3e369c09e485287a03b71db122ca396f2ca95ffefe730a9b6bc5455ea703df55cd2574d4a6d3cc0a894e19fdecc276190b517931356b1ba0eadea30652ad282a5e3c3ed54a2a185989c446dd560246d328a9a0ceac00bdbc8e68a601cc9a66d9c84248f00debaa2753ada4492ee750bff63a792a21bfb53851846db30a4e9f351ca6292410ecb97569a43d183c35cbf236f7f99765bd1697b818e2b6b94aba582fb2d54ae7c596b465a0128d716c9ab7c9ca949210c335c79037dcc21b0a67869d3791dce145f62722a6a334fa72d5a18a5d830b5237e796a835e1031093733bb08d50c6243a6436778a5c337b27f0e796436270eb2b31fbcc8882256c99c7e417f1852bc1c5729cc4c4e342337e7d013730460fdfd68b97ed32b41768aeea220dea9e738dfd521119fe7a5199e08310eeca5131cce4406edc06536916b877ee7a2c485e582c158443034f8e3c4dcc12324e92be504e0a9dee4940a79ca7d240b3239d267ba9429580ae618863e3a13dd18d643f901f5c05e06f5786b3249cc666ec9f9049a5a75707142c47550473ef0ec0457e404a57ea57fd1cf8fac0a9d35da119539704e5778c8f462f2b893025990f33eb5f7affe2a991d23363f70ce78529c14bd255f8b9e16d82c98b5c63d35bfb685bdf2e885891e1090b94ba91bf9111663175630660ff6788b83a0b4446f10d11629bcd0bc59b83885606a56c802ff05a11253a303622a7e7b16ec833ecad3a5c9cc4fe1b925da4718c425d4b5fc8e3c9ac700e1f9b4873e999e58982d2302a4722a297995f01ef89ae87e61495c4acd63ffbb24460c48ba7c7716e6598ec7e9b953b5f225b50925dadd2cc061cfa0fa315698001adb05464020bdaf3907982ceccc9c95c4df95580e68e590f6cd6074aad26c2b69cbd06bd670014d8f0cb3330187f77ffd360c951340d9c15fb8764cf69c9a47259c524fbcc0cfbeb368fb62fdad2ba0080666594880934d5b023d52695017f83484127c14493557c96399e4e44ac6dba0cd17b9ac5242668c3d8cbdf6f7f3e1553c102e9fb5eb2504bdfc68280c8f22b691c51b58a30c16f8d383354cc5dd1f4dc4beac7a571667bb3372b223fc77e18678a6bba858cf0af4ea53b625376542315e12bfc07f43b421c2faa876f9ae7f9946b2f92e6254c61a9460689193ae146f0b3e7db01ffb4153bbb53ae795374f37a3574b666ed327c65f57bd17c4ec8829b2ad5106e57c1832f5efbc316fabf2361b45886bdd5150097116aec07b9d8b40d99dcb052ea39c1183ec09948b4b3d9d8cc61ff03dc656564f2e7ac2690495f16fe597e4d1654b3e6e1c2b25f2b9a4096ae1bfbc7606d2aeee746cb5ff7506fbb51250d204910d051187f0eccaf179754850ca4b3aa0349826b91f601f917cb913449600ce19793528187f43007cbb2661fb3f5e791016a1cc02440342354bce6d3247ac46d415ee8ef3a18a914f6ea869a7089e878bdb9c79e1653b1d7c01e9ac58acd77a39acf1d167ab72460b0460446d1ed4f66d18f4104c7adae006d2cd1c76e0753eef187b68eccaa5473c3976e4edac42aa51427f438d48c63acb36aa241d049acb1d14e54aa5b8e49a51efdc6f1cb712c16c19b1aa587dd5a57ca15aac000d0fe6561e4c22a78ac591e2824d8e7427af2a4b9985c864d9eee19b15629d67b61028c050b1379e846b4d02abd28b0a0e72f1b7f6da1821f4f5d42cd37e9b31b4e17f4655b77cbd482a7beff9a9e41069ceb4833b610baec80dc502b0e3f764962c17cf09a652e3cf627f845dcc91951d3b5fce10562f8212a9798d8eecf05e78ea25a6569eabda9b907101560a3012c36cd75a8677747a6e7b095316571937c8373f6042b89f82a3ad8b2dd4b84eefdd368e1138c66f54ea475bfaaa6881a5660dd662ae867cf1586e2c4ba09e0187c0e363957e6ea865140e71c9d7b236cace102d6c162e7ebdb9836f7d7dac29cd069dfa682042effdbce81337c7c59dee174c43b280cc6e0f625c92d27e5f4aa3500d2332ed24634e7b574ddb7211346d3cf8ab2f6f089a324bedade76c062c2b0cb85807f99eb923bafd4d4d9ac183a275422283cd114a197b9451305f6d1579dbe7d955ec45cfc502fb0d886cfa210bd653f4ddd24e6360bd1390f263f47b73d4e56e8b03ca5fe1e7c77a493a49eb96c846bbb606af36de5a157c0ebfaf13dfe45588debd39666a605d58e95fe370f4889961140457b5f7b1c430a6f97ab50fc7cf92a562bc6a711e5a44541635b4367c15ade098b12e589245e2b6c2c9abff53ac3d94bee7e1a1e63d725075d21a232814146355e65c7fecdbff19cb4bdf2b2afef43d7a1628a81df98d33ffbd3c1376f203eec85abfebb88c8536cf05190bdc3f84bd273e8149f02486eca594ace37c613bf73b5a4b37fd213816e1bbfbae8c5605544ce39c4789102ce4d19f0e95fcd658adeefcf83d691c57ab437e18ede4bb7d595b16b7fc022632649f842a81f1e9bd0b35e27b35bd59f1b98178d7220730081cd3a19191d2013fee14d2c163fbf579328a8f4f2ca247a2164c418e1d4e0e1e70fca89395ecba69f213a37a70f2b8de087522cde7f910c8c4af8f4cac1aa6efde1fb3306068c658f06545f9bb104bff080e686e1ed8180da33ebefb49dfa3b5354f7a8187b71df174feffc07ba2dd8edd5f5a17fa6ad6956166270840e61174ac5decc794553fb9965633c8d708497c9ce5fd391a3c531e82daaec6623d409effeef7c8e127b0e96545503ed7be2d086787f55617be5024c9e42cda639dd3b42f7a38c8acb55a8535474b565a8b532918e3a2eaea1632f7f6feb8a6d4ea0ba5f943d9f4a254868ef44ab28bf43374dae244c2fc4b1290bae77a06bf55925258fb2321246865c539a61403617c5c1545271a0cc90571fbb6dd0acd01d5d93fc101a5548df5573b4c4aeae5f46599f5127194b178a4bd4e0036c3c6a225f4bae8c9fa07c922413868c3785a4ec4c6e56d54807320d142358ec202c9abf51743e84d76fe37187d03934ae25f5c29fa5cf638b5e5183f2f4630d9921e3d89b10a397db9045d31d33ac8095372c8210624831e4b66e6998c5b1a319ff7403f143a832a6f1d6edd6a423a9d9ad97174bf90cae3e9e30f78be20d40c71212f80603416ee539182a08925e704153d9219d42e1ac7b5556cb64e7f3b0ec677efb2b7221564bf6b677a9ffc140a2325909f38f7bb612c08b2d747dcb212542cf0ddd56db29b69acab76a83cc09b0f1d5fdaefd6d795284e99b1127aefd3d9026a26609ee352936a5a1eb8d8c41381115402a32b43dfc7109f40fc3bce9937cde2673d3b036ad543430b339226515584526daf0e1bd90213da9229afdfe40c7cbb1a334916cf1e17252f766da1b770991941910961e9f76c6e4cda72dcd487bfeb2db7df88caefb353ae19df2e2f6186d2bcb3b6879916de607274a9535947e3e0824ea069354ca3cec053b8095d031b1a31d209b3905c80b144769b1a61c22ad966306c94aee8262fe5856ec4b312fbaa897867103adf1c94bd4beeb61ec499ea730def3db42d921c1c413440926f0e77365c314c7475d4552edc0cb4c295d20901d5f7d9c9af5020357b6ecde9ebf9280ee1a963dfa8d50dbe7010e773b7a3b6cb9b497ac76c9401c6cdb87728c729857e669531b76235dd8d0774dbab1abebbf8d89e9c940cfc4c57687d465bc8a5cd768c45767728c71a0408d6f19b8ec73b1565179a2871d8fe919ced5f4447c414e6353cf6c1eacb5263e9676347fabd240e183eabb3123be5e0930ff92b69a6103284e728f6f87d2b2acfb1d5dcc698005ae2eb03013615a20c03b3a0fe166c3491dcd34786d00affd30f1da9d4cd68b9abd2a0b89c9cdf16655328e0efb7bc572313f1ee137ea1df90a6d64c84aa20521904e4094eaedb25bd9305367e98a59196d04179cd9b4955baec5fc568f0add2132d962a89928678b8f87418dd02f9ac02697e2c117301839049336ca046e71da3941234e155f10e820f01025d3c08c96e834497edee71a65c517b25495d5cf6b254cfdc1e96ec0caa4a8046ece54738d3f579cbf1ee82e95470a2e26977e7f7d62b3b205b8c4769e4003dd5be9bac25ad812242e3551bc55ac95ea025494dcd8d03fba5bd0c212b5a193369c890d0ee03fc077dba0360e5895503cd3e6737dc6009a190120540dc7b0b9ac30274a7aa5e5adbed3e60ef6fff78dcb6bde5c36e772afc6cd3c93c418fb05a00d2919cec6f8cd70212d6a0f2983f96412bc5eabfe3e5d702b02a81c0800fb3bfb35a93538a09b079954e0b32f140820dd5605331c797d7ec222ec67ef4ca1ad5e83377d669cd1d68e8a3d4e2e8d7f3198c715f15ed31e907374b9781d583a13cfcef3ccc0e4c10e9b2a49db33dbe0a00866d6be35ab710d6882a4d42c5909f75b271ff3d6ef0f03a7e9a96685088dff7d32fd5c2719e94b205e8f6487a66b1193a56a70ce6f1fee898cbb2d7ba8245ca55766bad46d56839dcbe14aca7394ade6c339e690ad6727043c9a10bfb8b001446b7fd41ad36696072dcaa1c6bd34ec8f27cce3f1698ba670069df8491726d07e6de8203d7d93932f2bae7a95b94f8e725c6e7bec0d8df0c2e80d7278a5f9bc74a3d934ffd92c02c00fcb8836f03c59fbb45832d5f827cd9f604528a7b951aec7820ea9a45fbf35e184b071d369c1d4947ecfd60df3f28bbbd275a6fdd768b07df3ec8ce09e3e2c34f3659370f552abfe141fce7c91b02b7416514804f27acfeb0a28c9add7c41e9b9023a85c4d8f01409099e9d953f4bdba46626a6d77c13616f43cfbb7acaa4c28990586b43bb025b69576e02aca32cd770b2ad95fe13af21223b90ae756e392cb0345db05e6bda2ee143a80c983ddf642183c600ee4b8073f53d1cc9adfcb1963ecbd11a08bd38249ae131b48b75b7e8ca34d6d4c644a48d309e0148d3ea24d1a0cd48b5892c41eb7b35c4e72e293dc9513ef1fff4dd5d474807202cf2879496d8546900232545516ff33ad4c8ebe166ba73c0bfdc3a5d73312b04f44e416a91ba8516049881a4063502d09a12b69079807033e04d9d120336e7b9b37f2a5569c31b2ce3125e956169eaf4ec67867c3c685a258e1a5786e65457211a1c817f12405c38b8eee5e99f300b8a45e9e6f8906c63a045f311e896f910fc2e78a0805ac4057fe3bdc988e90e425085793a7353337171d1f39c7ed4f7ec2908f74928d4c46e37b475994aaf43e818f0d4acea4ab483f098e54aa01d9d66ca699938e55ab403d25f16e6ec3b1f369f4717f6f41b290be5e9c3ab9adf4d0973a204728700bcfbd35866566ddb29da9f7a8f3b3f182bc7ee0b2e5457ca72c686de3ca58469beb9b6c7b20657c6ad9da35aae1915567a337cf9e815faf677a594cb71d6b10b8b95fa3e3acdb4a98c7225df34917c3c4413758c694dd75663cf9a2995bf089824d34c9f6158e6f76ae5a804ef053834c9a293b8011c6285d14dd6d191168e9c5f884261e9fd25a8bde1f51fd33ab6ea2de56932a905580e4f692f9fde20613f2ba2e10975298956f2cbeaf9bccea34cfbe280f13121cb3c4377df8d56b539e89913b156156f4adcc732637a7922cfb4794dc085270307b313aa341fc5349bd4720e65b28981bc1e73db87581cd57fa8b8aab3f37cf40d5b56974023d9890a81d7bb74c2c8143ad0fda974f83f41b372a1f1867f8cb1644739d696011620ba1799bd1bcc29ea3a235a39a2e4a50fcc901fe31e7bb74453d035d4872749da959e21bf96c4b1b9d86a8ea1269e9e41a5c4e464f31b07b273f9ecc2dee1b4d578bad1b18e43a285db9923db9a1e376d137b74a3e302ffbb71d2b3deafe875885ef6d03ab0c944496ab074231e369605b726e57eb7311b473b3e73b403fb43c46a1a32ab5c779a1ec922c88d813deec3f9e381b9ef41c55a46bb394cbcaabad44b8dad1b132b82d05762f89ff56e048d913b4ea9996dd675082b3c350925cbfeecd0f6e8f025d5ef14abc143a0116e7d8c63c9e5fcd46785b1c0e799492d6fa1af94206a54cf38c546c09eb198abb94a6d0e5bac5d5542677e31cb5ae5589e07af5b6b3c16c0f36774f915e8c247cbf379124bfb125bc99750d85942a65fc9dcae1860e7db0fa6bc94a2dd7193108b7c24ebd5000c0effa53f267dc28a6ab8f6d1a9553e5c66bd7e73cf53d451d66baef8bf17550bc2cb30be709ce180dd369bf728e657f06874b432a7f93d526af8a6e7c42cbda5f35fa2c0eebea780b825f074badfbed4966e17ce1be0e53a832fb35cf1c4b21c700b270ac540fc4f399c3ac87a3c32c4b7cfe54e0af800de893d14a8130fcb8e118e64ab056878f438aeab9c102226e3be5782e9a8eff2ec6b9d60f1c1340511f7b620e36122941ab15ce4feaa62457e49f5fa78c1e6ca2c8a0a134536e13014b43001e7729b261b3536434b97616c4096271b0a82b85cf12e3e04272e2c74836c61864342cc0a2e824182b0bfc776f50a3a356acd317bcca78858734c053bbf4da8c33be3c029e9c5b45de49f54c97cef24a2451cdc2bb66429bf2e232e788329d44b9dff0ec6cb61902eeef2073b5391ef9da126c916d9c4e62f411b5b714e2351abc06bb8763e01c9bf926e0bac6f66faa5e8555f876ea6686749e30ce2ae1f93c249899f3f692878f316a5abd9083f357ba92ce2644ac722345d19365dd7e5409fb549105215121729c23973e9c5a8260175c604ed10ada8784b391f011d5984737995c092f7aece943a6d0c26ae7274884952604ae60656090698d55862b3692b261a4eee660a323858b12649ae1e9626604fcb27984c4da8e280fcbe548492d2104abba2d3224550942090e219bb51ac8cba9a79bf6bc61ee4f50fc5c548c6508f57e3cac7ff90095f5d6eae11a8171da579aff45c87281eef281458754bbb4fe963bf88630757804f9fec7e31d1057c8d47a840d1d0a45a76381a75f8b079a8743cc05fb03a4f1d0c313b3541cdc2493a708ad419a1ad51824a08850196fba5aa95c967afa40aca6a9c6abf26ff7c4b7891610390b3090c2470f451046c9a438ff3e06714af02071a379379fc107594785f3a235bc8417122821e8fc295645132e2736e82816d09d10be030474bb82baf20c6c8dfbb4efc7e6282443616603d403ba96b505d932fd1a8e6dd64c5928fbd8c0863126d994041e0b8160a877a26242afb14c35df93b526d620f68c92bfb467eac917633b1f5798ba2062c69b27b6f44be39acda81442b2495ec3a79259262bd5c28ea1f4dc77eb0aee5cb9c1f04d0be9d5a2ee97902da6070fa90d826b395bb403e1c662b306b300bfec39170ff39a2592b87e16c6afce78b1913c84130bce7c576198b2630c724097d6acc25ab843615fef38130ddcf256b3eaca81cdec8118d9edc669830b4b45c2a3e05ce7e1041ef0b34166786e9fe3f09f1351d5846022870c6b4b1b3ebc363c47a7410022a35709b4a9bd4d282d7759de091d041e65d764abb79dd5e9003fb6131ad236e0ea0007f669a2c6333626a7e107fbe3cab2433513bb2a7deee7ed27c14ec607f3cad1aa13c9d8fc792c23d0f9c66a37d8a46589bbe0a6aa9c0b0a2d17630a371d13d1100dfaa8245f726ca871e95c3270ec116706d663dfb440448696a9de51dcad6db81b176d4f19242416d79508a7f4c68e8d9b7b873c6cda568a499b317bd3da7dfe6b0b4fd36b08435bddeaf07f64c129a50830802e8cd8f7972aee493c95247e123fa918f8f8d0c177b7e12bdec185aa830d6205daff844c18cec5cbbb73fef0b41edca8ea91157a1902b248e4d32faed9bfa714d7143054e9c12a55fb2aff6eff6c39a7903a1f7539243607652395cc62cff38abf68f6737696de9371975560496c28b823390a0cf4928248df4343a0a956a77e919dd918d556dc303f0b8ca659ecb66711da56d94b4b1dff14c580891f23e2b1b8e78f397a6bfafc2dd1e01d256fd9c9f0086b1c1fcdc503a2a53b9cf615fa9bc4ef4769b26defa42e890949a4bc79d406f0c0b9955a3ab5fc9d541f5078738377cfd1ff216f8316c7014628d0368c2e8c8ae53e5c3ff41aea8100265f4638fb05cefd21cb889fb59e1969683ffbffce9227c9f7bbb93c61602f8ff922e223b0ea18015c600d65bdc9be75c6a0d474b67ac7b289f8246095d3e643bda84f82f70cc9f068d3d07e8876c8dce308fc04ec84a5a509bb9e0d566a113837f9617f995efe231bb3d24076edfc9f61488974bbda046720d9df84c39fa55e494c6fcc5d89dc5b3ee62c2df54538461c99211a2f74283cb15c2805923b8480c4d720535974ee1b716f417a807886f4581c17726469ac06fb9f3cedc80660299cd7f6485b65a43786d9f86a02d790201379b4e3f4c5c1401a362c6140d409f21f4c4d2887a3926646b2ecd8d11398aa3e6f9def2fa2172cae1566941cff209ffd3f92120dd2d1b94f55429f275889066cb5d1c0dfa348e9862e98cbe97050631ac30713975b74e8eafe52c75fe16ad7948fa81e3822079adc50cc78d0ebfcc60e18706d1eafe36881591fb90b7e11846a4f363ca06590753ee2d631e4517e1566e3048687516ed0ab8311feb02a27dbc91dc18666840aa3d93f20d5ac6aeba79bd3539c1988e96de4ae04cadeee88bfb2ea1b8fbd10f46ce6544cfb0e75ae16c6a966b920d3cbe55a74b8350e28a64519f3269cabefeaf67d4ca6d68ce4afc6415b02a668e5f3d0c8e1b5e4681e3352626dc52ddff118b45f96487408a6c1adc71350bde1aecfcfb07a43f6557c69ba900177adc13f8586f17ad61c7ed337318b46e8df0d88a4b9ce6cb5b9d34ff8fc67a681cdfa2100d3660d65dfeadf9271a2cae3e3e55fc4737a1ab7e8d10a4d47db46026160f1651a83911e400364e4bdae1b1800268878c7fe391759971aa008c7824fcfede6e86c13d92b40812d53309309c7c88b7dca4eb188c75b73684354445dd5d4d5b52021fc1fb190ae8b001cc91c754ad26a60a758882e5b9b3e8fdb841e89b9b84f88abb7594097ea8165cf130c14c2ebba98882b05e6e44f7b722229698d4af7c02634ddc7440ed7ebab67cadc5b672ec9f323646bc8e909eb1b5d74b9dbe9b31492a56a9f024756269fab5e326eb497cf31667b9ec6b7a3bd9e12da280fdffd33256a777f35cc8effadcaca44305bfc0e9dea5445d68682c23627009e2fc05a814bbca9f009065d2c54523f65835198f98cda5afe07af549e93f098af438e830191e03c1125d0af0f1bfddcc4514477ad286e54745000c93fc471aaaad629f46c45daef4c322ca87a8ecf45472c99ece1cdc30b804f0ec51860ce27295fd5e4e09d622e5e958446a170457590610cc51bdf784058cde299a9c840b2c544f5198bb15b4242abcab773049ebf68f51bc9618c4b92638157e78834b2dc97ef6093dae2702039447559a6a814e7d68b324ce37b0e0460743fc04a58b7730deae334a9bb9da7f4cb657c4d6cb99b816fe39f7d3e1783f39e8f383e41f5bac9a8def8f7a5651759e929801f3066a80ca75e0c08c8e62f87faeaab65a3cb23553257c31a854861e6912311943483ba2406e185fbebf1a598115c2c39c702069934a008a8aa728c3bafabce873c0fe52adb8f8c427dbece0562a973dc6e8463763c5352320f332af8e5e9623762d723c25d43a67bd7b36561db44a7cfe069354af5f93893f5123bfa34bf07641c54d2a853479ad173507c4dd4849ef094cbe7375fbbf14699825d4c6034a9254f7737d3e1c12ea50d429c46c69405903c932c032ba64010e3fde0f56e28f257a946a00c631660f1f4f077c01c2fe57ebf76a2e1f0e14c26d712ae5f6c90d41167766bbfca8716074b1e6aec9d180bfede8b694146b31b471461aced6027c7dfdd22ab1a6a322f64125d48d17caef1e87e6610e5af449ba03718cecffc87cd3e24ba0b5958498c4b01630def798b930ec6e06d080f720da579a4da7cbf0ebe8f23792fa0c75702d1ff670bab42fa19ba115427b1fee96ced44cb9059a5f4e5413f358c013a853907cec903b0f8b9adf7c9d3f3b597f806e0bdf0a6edcb114805655e21afc8549a4f824b3c7e0e9d70e34771d9666561145c0b6bd9b7aa762dc8ffef7888ba10e799278c29e2dc52f8804c21b8c96ccfa3f93473d65dff762e104f8c3ac2bfcf84f32a4e1419903789e7f589a6d79d8cee4a26ab502ce307d3f2bbd0f67a67e4d4a29da9d54427afc7d93ce1c86be5b100cff3ea87c56362cd1aa9a188e62ed39d89eb0438e89bfb16191071ff12d7f7c46de8c49457625dd6240629ab496a0079684a340dec25850f4bed4ced8c75d84f52f425fa03df73037c2f3bad8d0084395409fab363737e320d904246b145d89fc63f1ad06b60f2c0274996bf601bc653204cfcccfb6399899190e6a01db6b492db35344717ea26fdd3b75425f41688bec1973803dd95b1751c64cd5b30c58b9aac36786c6a9f1b1a61a5ad9d4d02ea0437aca7a91485af7802db79510aa82c6633666c5528435a8b150911e94d570de6c84fcaf317a1f66a71b0a90b55647c526f980fd2f56d26342879102a4a1cff84f14ad4cef3547af870003723b8ee542fe38d0aaa060d589f6f7c1a39e93f1daa604f5d847fded5334440458ddf65a8e6f52da10d0d5fe81f7d3dd55aa33891740b8e83d61c0b083fd467d3b064de8637d857772bbfa7a1ac77121b520e9e1bcfa30063debb899dbad45187b996696d35cfb63cd7707a38762ddb93bff17703713e3cccbc7d0666268b3e80c0d991ef04b72b0a1b2d1ddd1261d34354704695d46ef257c4469a174f2ac21b8899c015243af26afc8ed6e2c521d6593ee76648733c4eb9e76efc6a9ef80a1aadc69c55b0a52d85a3a62de16daee4964d8a6d86548002a32954a88bdb6fe66cea376d10025ab1677166e4d37d941d579998cd5d35332800f0f77898ff24f0d1de229cd4247a7c6d9ce4a0cc50c0d82724d2dc8260f9781dc25100cdc6a3d763e1b448723b7dea929a118c85e45f56ad59e025f4a052f194528b872fbcc3facd0b6493583647e36eb3c2b21c7c6ae8cbc32517cf5c6c1ce84ba2714a6b5f2d879e9a867e771d02257f3a047e440e21ddeb58df95fb47a68e24c06599e51f062d84e3c9fe9ff7041c7792fd4341b343833fd8693f4b3c358581d46e8397c44403fe2b680f9c5885d8a0c1a326902d58d7b216682ed0c66dc158d62061d0532159f93d8e2e16c9d8e151ab6fb09d9ac63a5babd497f6029bf7e2c77693c9de431930354f6eee27465c9a9d375be7cae20ca80da70b621d70c833f135665bf6d77241605b6c5d26de6a579cf166c8b356d3cc37b86bf1b15054ea364f24c0633c1248d3612c0fa8736d2366447084f5e8e0c58683c78d74089cae5d75d47fecad75ed1f13a73d4270925115dc734029426fb5f634c09ec624eec5027c723f625b95aa9d0e00ba9cba431e6072f32e33e6ad6bd2a2964a5a613e9e49719b5150d887a010dd7ebc6bdc72f3fb7fafae2945c6cdd5db819743142f02aa5eebbde3249be83c7f8628dba350e9e3f97b52ee765c7ceda2963f972a414f07a2722472622de7bde321fc2709031bf82cb992024bc8913268003e547c9a4cad704e37fb5d18b07de4a65350267c4bc8cf22b22e04bdbb7a99e0c1ca2a646ac49a11854c2e1682ade839751081cbd2782885b549b43a6cff964aa60f68876144670eb25a24aa3043445cf9da46b294dea146c5620a04128d468958eb1099d2312efb4d3a1764cfb157145d27504daeb913f4b378d41b98da168157b4964a593e04a01501bca52e61acd6521eb0106cabcf02c41c61c734ebbf0d1508315f186c12dad7bdcf9a2c870d1975c76f1eb328668d9eb05e78f1e5f9839ff34c785e1a14e9534c996f51a77a7fd1aaf1238c271672a61028a6704d28267dcf2e7b674afcd7578d72414094bf8df02ff903302f7d98080a5cccca1801fa06c9c745a77a7faf8bf40968bb91b4f0227fa0dde7a7695a56f5a53136b5951ab4219ca2812a6ff46129a2ba9b1a2e810942cdcf480abd7e682f1c3a982eef109c71fc85f6ce6de6b797c20e147b8c72e7938cbc0f9fb2f67597d17a97a625ba06216d6144b3cfd0aec6431371dbc764397c420d3c6773b9d3ef29e301d34fb2587bacc70a6ad86088b87ea82588e7940bfab60d099213e340b321b9327a1cc7e107651859c0f22881848d2480b84ec9bbea4606a32aff7d683e1562f78e9e133791ad6352bdb1554880946f5de0c93b0ed78723cae50fa8e5cddbb7e1e66ba935f1a6f0c8c7fee6397f2fcd6bc931cee27173b101ec6026259e445ab3c69f1ef76766ac82690b3ce46303319c3f8b04e181fbc70bda6da9734279117c966b922a1db2527c25fd24bcc99ee8f631587968c57bd214731138cbbe30091cff559d07c1961a5203c8a9d38795624ea7a3cc9325afab5ab006c0b9ba8b1a94ae450e00dec05f88fc0a271c57063780593b8dd611f710a94a2c797b84e0876ed950b2c7f5e5dc4b10dc49b345029784b5d91e604df3268428fe3550d4fbdfc5272dadf34d32dabf07b004fdf7dd5a14d33921557403fc6014f16db0c318089a9bd8a4f7f677a43e963e4bb5e7c7115393578aedcd111acf817701828f6014c936f47974adc1403f2bf6140f8403e423d4a81d04f3877da26a80badd413b9aa4a172a4bef6167f98624c12326aeefb4a11f7d58c2362fb95b5357c0e3541805a7d2b45a1c6d1d492307c0d3a0c0d1f78260efe0d331f01f32062694d7aec0e5a03d3e07c94e636a0913a7a6538eb59167ef58df36ed857f65d515db7dd4c365b7a771e7b097fc2d4dcf1e23b3ae18d968362a850f0f9d8774906e89987a1f2f9f9cc374d7852492436c148d9cb1b7140aaf5bcc190a6e87acccc2afa8e424c559c1f895c172f797d1a56ebb43ca0067ad8531a816eefe8b681c2676d262599a59cb432ed47dc03cbde9d9da1414bd9d5beaede1f7d328bc09d74a096f5884b867a9b93a4d289980446834d7704840022e0108a9edf7e4d577e3545f475ee931dd051859a205e9ba47d381940c99941103df5bc07e92ea3d4c41bd73aac0d6772625e581458d52f2e9a24acc840c21831959f78257f81ca86de1b4fae62fa48153b1eb776d0d97ad7b187fa08e80b887ad1d6d8b104727ffd12020a8f770a7d1fdb73f6495bf7e17423b1f648b4468934d06ac69414f424c33014c1ddd17adc06f0056bab1e05c84efb98bd2f6dbf41e5e45a7b6ad8e7a77709654cc4306e04ee84daf3f7ff4a8293c294c967642b0af1dc073430572bdd483a085dcf61cad2f79e27baa00bac3f0f3e53b138c4bae201e5bc8c4d26506a8ebf626cde21a9884d74c1b05b86ef9c12cf1ad301c18d1e73c49971d8aa0bc547e575475de65097e421ea503bb86079fcb97b60deab4763ca843eb072cc71305e2ec07b721454ec66d80a888adb1313d078caf7ab83a455b0623748960364b502e563e6eb5932103cfcda3170959c07d30f90b5490f11bbe15521033ed6323eb352a9b635dfa2e9b632590ec43a231a3877e8b00739f4e363c32d3fdedfb5f895c28939d15875018b286f09d70eb9284341a6b2a249eb7aa2ae325c8fe2613e58c0be7a678e7105e7b858e77cd3bf6690e94857fb25377b509a5cecc639bc63da3587e2eb360faaaa4a47a55c5dc082e02d605cd6fdcf270963187c13d7580f92c1e44aa9be7ad36e1c3c7f9fd296476dbe5665ba8a08af961f63c808977c2a8635283cd4e48b4a4f9cb2b933504da9d12940322234fb9d0dc41bd6c9bd51037c75fd71f4e88432655a67466ffbddcc6f0697b32140b9410bad8516b282fc541d4ad6b7215cfea1a6f831ecdb6b01499a5ba3f0f134a07bcd079c500522e1ece1cb037fa5faea6d7be7b1cd876a299db84e1ccd0f0602363320185ced2ace1c75afed335fe472d9ac30e05776afeb41360cf7a954aaa9663be207535aac776d8cccd95c308d24d6f9ba09c1a53adb4cf8af7687508f5dcfa057a40eadb7a88166a0145b1524d63979fd8495050065d43d73ffb1f3682fa218d602ab60e74df375987efdc65ab49788da41621e1bbdc389b78ac6a972328e7f0b856801a630d83d7aeb7cbfee2c4b10f396d91ae317ace36c0de26264e7d28682eb17c6e0dd72a2c161898c386a2f116eb60c6f52aab05ddedd9022b797d03eb52b231c17c133942f5b19eb2111d7506e57d990e964bf6f19b4d48482ee4b6230bea9758f8d7a8aa8620dc9495636b46aefbd1d0508762098f2b7d3c4b2480955bcfe0a8be45cc44e7cd949224483a083262734df2ae7ebe560909fe1443af61e9f289519a3fdcfca6447fdb118d720a74b42e454bde44cb2401fbefef3f50e03d0d28163040d9e23402a52dbc1cbe4506a6fe2e784d1ab36a770c8a8308754c3242919bac564dfb383913c99d18e3dd3fde16bf401a3f31fd4837586f64f66c83c8ed3457067316a6678e3ba79bf8717902f32affe98827272d39f1c04ab866c0b254a2b818f274394296957d95c8067c923dd6bbfb9c4a4578022d2870280917ffb99176a1c889cf1eed290d6efc246edaa720de8e75623dab594d17c6c4bf2650c9f9b2e81acd6f0867b42aad9841eadfcb53501e28e18d014e7b9ba4315d227b4da8e4e775a89403ce78651eb12c32241ffcf950f57ee67651ac93ad9460a28233ac2dc7e55c366e5f4f2d60c6d75196ae9a84b5b173b5e76cec06c9b43acc8a98e538539b21049255323802eec0249b2f88cf56247576924f99d543f840b9929425e26100101b36acd693359b643abaa255cf1dfb0085afe4891ff61732dc3ecb626d1327278a07c115329eaaaf13f4ef8dfbbc4229fe9e594035867d35a5f81a933fed9db541a08981f5dddf12800e9b0eb271940899ab61d87889543ffe3a0a2857aec3e78fc95f7c16dba33eade6cf5369ce29261346c2d864f560122d4c3dc97fcf718760b9637bee060c2c2e9770f6693683ea3f6dffd687c4bd34a413f7d5c58b27e017ea77275aa10ccd8cc78ecd9463b8ffc8325f6035f12f33713d0eba6b3ddd960f98ac7753b925382a9715906a7df8d5981b631e145b7a4b9ab019e9208f0391283000b733e310f458c3428be1c1c4f1aaeedd6ee9bb1ecbbafa4091d4dbbdc8430ce81f346cbb470f86e272eaf97040084a47f35c81e6f358b76f33670810c4d1c20ae5f2a0c1e580652f2eeffd8ba48dc76162f8d8dc9896428dc38dd5a99cdfccbaf665c6f65b811716f4e268c313bf699f9c57df5840610887eb6ab14cbb5aba9bb3a7ba5119fc9e5ea00e50330e842b9ebe67f04df4f356e7d54b5bbaa461c9d2f72a822c12c61d10fdfc91303f27913668e07f5e2aa6ddff9bfc4fbf9829556d51db8090c7216621e5d2d0a90b9b5bed57c8476259368dfd5c49693a40ccb8af5e86d1cdc8e1508cbd22b705a1c0a13fbf364b2e2bf1f9898d1f7afae4ddd9ab21d8e5f0366ef549e81596d452688bad5a8f77dd15a5317ed622d8f9fb8d7cee6fdc4476b0516d26e0f6b79c0638ec2f817071d755f5bef5110d14deccb3a719d9b9dc7625e2a443de92e41fd8773c037b28778c06fb9cbdc5559caae57ecaf99c4be0e6b67d2ea7869a35d9e950dde84a2f9f2d22c801ef208cea147035a8b3250a703e62369f4298cd85d8fc83a9f0e4818986f44688ab1e1437475b0d008819b0ba4b4398042f355a95402a8d2b38ef6ed2bccdb27857e56600dcd0c2ed30432a5963de6c188a50072c1c4e4d602482feeea584d8c5997367d0fbf693f0bea7ad5f05df3a713e1685352d505cea89138f70cb619ce8b0e710c44ce9e826f65b0ffaa1e365c9095a886569b234e1eff5df8d778cb3db3263562228464c6e5dbdb2298f8f1cbf1ca20dfabf8bbe25a448f93436a56cdc32509d085fbb5659f33bbd6b08641d5ce52c18d0849b8e95473881cdedc78bae8fb0fe95184d0c7d1ef29aa8573a388326cc87cde8c92af1776e05dfbfc8bc82ff739cd8059b9ea62a223684bb274ea5405986f7c1ee17714ba35f716facb4c6cd03cb71e56d3ee275fe4574a630e2215fa178fceaf0386754428cce33446448b2a1f1a45af75213d85612b9a5ede557acc94e661578a2e65ab13987ae0972ae5187293b02ec9d0a8a60ba9ec024776b5a30fe33211f20485cec23b23d9b0c5d34720cc9aede0aedad454a92083ee14fb18b3df6636c59c23a5f433fcf4c7b9f5b5434cf9b69fbeeaf946524c16344bf3eee7bbd062ba58067530237df6da26499aa919fede9693644ba7b39bc3f50e168b05bed8472f38fb3bf5f91b16b230c987018b3da4a84dce05c8489246bd69fef5297a1bc3b6a36bbb0d8e3c51831a777ca42501105d6e049c752d1f103883ba406b9ceeb2a1d98bc08c052bb209128aaf582b26f82b7b127907a055bed409aa834779f5885eaa35d493c0d88338c62f739a87236f4375d2df78f9f306bc155a846dc5edaefd7b7b549e20b1c312cd05d7929a6915ee87aa0ee8bd09f9e4fe3078c2b0aed95f04a9ede1c3a267c44199c7c2d6535c2cb84fcabae82fa0458fc868db0d44912faf33f6f787069b8dfbaa5377d0040c816849366161f1b44b4080aa5fc8fea8dcf4692b3261687b37923088f0a134c8a8694360b5e095bc94b20461ed9228cb4d5ee0015c5d93665330f113766eaab936f76d1b729cee06e3f161724cb6bccf56d937c45b8a493162206c90060cf84a986710c4c017c955f7751979f5b22b7990b804c25cdae3379aa7b831edc0d0d939683786ed556cb8b8f7838cae3d54b20634dced0746b28b09d01d31575475ea59f0808fd271d4ffe9c292b8b6c13e1cbf2fe94f613783b0ec79c0949635db34862f655199130da09868f4acf49a66931f961b583cc74bd2a40ec84016a90cb9a39ed9b91cb1679012656f5f80acdb0b08b3405abed212e7d89e574d40dfc91e7902e9a3e3860fa85a0de8aa701e0bb9dd96437ebe879930391da928c39bbc7a498aba29641787205ebe9cdc81915c1870588809a70e56f102c4cc1d852d2d78d784672fdcbb7c75f6e3ba641f49f35946347032aa17de512115d0db2b516d6bcf528e215478ce5fe26c67d277a11a68afb960c5f6df2f436da1ef86f643ab8a1d56fc2d5d502858a1dcb051ec2fa302cf1e25e83e386d8127f000436d1b52bfe3da81e9292a610f28afdd8b34a5d6f2842de35edfa05555d06ef8b6ffedea3550c7c2569cc9170154033e4bcb8d32e3d8038ddcc3b3f283a22cd36a06501c34858c843c5108247c8fb49b015b50c4d87af0b4169d04e71f843a7724841882e9701b77e0cb25278d3838240c24abd214abaa279b3d15cb85a8d4be1f7121438afdb9849395af64667ea40b5db1e7c41ce2fc4787f7a63240cb8074d66824fafa33665a9dc7fe10eb57b8eba8b900a0b3c1a4d6b57be638436d1ede19f32cd39d7ee0dee752f33c80cc8d37497178dd90a442548d8fbd16e06637a565b3c693fc12f46ee45570296f59ebb7dc884a1c45a7a47c9308fa263322519277aab77789a1e5a1fc5899da456b2c86869743b10248c9177ec9898c92eb523101416caddc2a76bdb1ff7af84c970584bed866cc90a3a6f519dbab696178841171ca7deb4cc2f4b2c11d9563b26854f60df99925b2862027df3d8a1c4b37c3794f5f6b5aef9f02d4f322cd208ff0ab0dc687afaa6117009d382839fd54bc13dcdb1e51b4a584f07d7158ae15056113ae06638412880e01db4d47fc5471d8a4cf93b674e22074d03af475fb164e214a091d26899ecaebf9b90270623bb794f00e9c0f36993b68c2e9bc4cc7e809fd3c418c1df45e366b4edc2e7f7fbd063365dc531d27a2d7648146af1bedc596f6b1563ca5ee11597d66ef0dd23ab9e023089c3016df9cb6d3375cf619fe261e910583ee78496f568f22334e0bd3b8f2c22086fc8fe6b09260e58083c70c5ce5589733ea0127110243e30cc8606065b33f3ec2873860a4ae80db49c4cc74f6c0419b3b3a9850ca34f5c695327cc32ed4948a5d8c21aeb29aeee7aef0b000655f2a6650d462d80a920690d866ccef20dd21d3239135c43da3d90b00d3d28708bc3f734771e0e98c32e8780984be5b0688dddfb4f3658e0078fca05a8ecaa7022b2d9b21759f7b9e78c2f544c014eb4954c36459dc766464c4f9a616775f36ca3c36323b5b1f25375bfb8f69da5d80ed210cb5a3373d14c51d168d975d9324dac7995aacf4a678c5878e73c0fb3cb54850e609ced98c942f3f34d6d06b93ea736eeaa6856dbb03e707687778da056ba1604d9b8ee7380c75810d1c84d71fa55978dbb012e5748a4be626f2f81d8e740a444362a7b780881d67e99d4a37ff09f01779ff4557a4096a88415b6f690b861b01dd1a02da95e076c0f5e9b75125b852afd4b24d1e0aae5fc07b0008dc39c8c9221891c0bef400c32257fefe38d5af84a9a683f2b3ac995cdb517394709a4e037457490730af185965ae2002c71687c971fa034d7ba730724548f7f7da9fd8d9798f54c6055597569af0476fe57fa2e9cbd1956d9e3039afeb391d9e94c1c6f58c3d46a8bd4655d8f87a3265f9d2968a29f9a035235c84f81ca57b20935115ede60aad6ffecfa2d59c005cb4ba35ef7fb092692bff59afc7a54c990a890c31a86f9a5211a7242a0e478906279785354de8c12c4b7d5b22b3fe05dc6cea7906f217714c223b791bfc36c306bdaf7cbf61dc050e5e5481c604624609d2deab512baeaca0bb8eeede21f09614c2932d788794a1aa6469bb16cd2f4b2f026a584361120fcb75a14e02a80fe5c79596fc3696b72a9e9aca0e2c0a10b50040696841a9829a61710c668aaafadd45394de9760eefd787e582581e580c357c31a6836369bebc79b50bceba9716105d5c777b7f014b41b81850d2a5b32a1fecd24a102e25a1e010fad73b3b80bad681ee29b4716a7056919cc95362c5ab5d71d4e850585c36aa1fc276c53e411e34f71f115ec6ea3b3e7bb8ca7ee8d2788301921caf7a39be665a0a8158b3c29974542b6ed9f2e5864414bbd7655140ce0d6a12321a1c9aa4342421995681c2c53b7d54e5413084f34501b990277f976c84ed3517b27bfb46e1cb7aad640114a96db80313242fb1be59b21719edb7f8d388e73b27339adee0afffded1e6d2a468ae6067ac1ce49c6babe86471f8b994ae007e530d291f12a0a0de79c26ba62e1b3aa69bfa6df7a3a652fc372699dc37c3229c10cf3d6b4ab9d7aada2928094ede34ace36947089685c9d30759de34d6016eb5e3f6b8aa71a036f3be4b7410cc971180771921f5991027b4c04859e9e56349fb0f1b2c75fff1738fb125de137bc5a372db4e06e4aa7f5ffd6323173df535856b100d3e6fecc15dbe04936faf5c6ce130805131fb064448e3083fa58b9d934566a11e96b6e1aca5cb75ccc1d04b55a2bf02b0d307b13aabc1c90bf8b11442131a314c491d824f753b87f5b03032c639b2273b2cff701d9f12c0a2ffdc063e9397e600b790d2f03012d1eb8219d2ddc5b588eca231728cc127b04b86ae54c2c8da136322238c435b045ed3a4d4cf4e27dea40ba93a309bf2dabd6a34c852d544425555d81b4458d706ffb8433ab7e89ef65fe8de92feec735115cbaee2732bbff5d18503547dc2ee9b945034558d2fe7fdc1e2163aa52dec84e38fc0ca4a53969b72a2da0231a3e45706f32e206d4652a3b5bdc3cb6ae10f45a60f3dedebb2c57daceca1daa2189c40419cef9e5cfa20125affb434f4a4f97762323513eab488584aa297f82296e301aba877d9f295f9cc594da59362d5dbbc26505084d94cf16d236f620e1cd3c13c050c75a4222ce8989ab6718a85e70c94cd1a6fb31842dcfc018b65ed54b50e1ffff4538d6da1a823f6a6b34f33c02e38dc2534fc20ecc36b0252a6ac10b69cff5bbf683b2da351c0013eac16e555c6d93494556f8b3280f883208a5aee300f5a895c0fc13426ff238aca979f4ebe7e0970444cf91f4da6a2759f91dbdb062f9e7ed6827d9e0d3edb09f58948c1681a957d6d09949b6fca4aaf767e9dcd43ac513c5646b8ac58848a920fdb5da924e9c4426f23f2fc5485df4f76f34597de068081431a1238fb48d2faa7e44f0239a0877867769def5f353aa262f44f1d8fa8ab7a9ba43c5ff41a3a7396b50059328cb012f5d73efb0a5a4c5121faa21dc0a31e69e1b320625f927d076ab81ee7a6e7b67840f682f77f88cdc5da38af91da5dbe6b845974100be336094f2a28d3fbb9944d23a86e92db74196fe6e78402539072018e084df8cc5dab6560634a15a62bf44247c1e770228745bd10a7d1309c847aa20cf00325fa8d3ed923ba246dbbf06b173fb324cae3ce3ef46f1bd9b4523fb42db52c171eae21e10ffd5d36c617e6341257a25d15e274bf551f754c05b5aa439e1806b0e45e97583a1f8067eb12e41baf7482d7723fb21b4022163a594e49b06510c4476f2bc55095cc71f4dd93bdda1ed4a0772eaa3c1794e01bab03db3e477c40b7658897a093f24c29ea726eb6630a6ae497120440be3bc63a87807480b8981b32a7469c8d11e353406443a3e333c0b2e4a41d44d6b6589fc21ac995baac9a6f7739a57ec426cbb70762af77bf8969e2d32549fdf9a737d844b63dab655fec2b4feb2ec4a9db32117dfb0feb5235bd428c0591827579be0a4df119544232b65b7ac8a94ede070fe65b11bd1a42c075a0a5e62cdf023ea95b1d2af85fe4f6e7bb22c777d1d410dd3a37cd1762912c4f4bb098e39a5def1f9d7445118837adea5260303db1a5c91bebf07f9504566d27d05d743357ef3487722df102308a50cce8697f7ffaec920f5046424c1b2fe7b311fadb7b2e73d21677cbc37bad3dcb198e55dc80dd9402646e90b748fbfcdc53c21d53bbbb2775ca8ef29f2da00680137f62fd92081e048a20c8fead44ffe8820edf7b2d638926279958d1e946e86bcdfccd0f172bf66cbe385fa7479c4e38614ddddb9a67349eef3e3a3fafa50d3852822d95d6ffb341bef1aeca5f394c5df7de6318e9c5ec5e287a5d17f7b2fcf1d4a0b62d0f0244f8f6228f08e3e0ac2910154bb4b7a62986e9b72fea291659627ad5b45441f0df086b972413948d1c1ca7245c423ac613bc4c8af6a9f4615c019cb58a7c8ba5ce188336f1400019f11be5f0dd938cfea993d09095057fdf9e9dbb0eddfb4b41c7173be11ca34576e8f943370ac87691266c4138a71567601ac14a7b4333cafa00a82bb205782df319d6359ebd5773d1835f5e7cbcd8a78665aa336b7a096fcc0b8ce9b6835ca80e088de79cc86bcd098111b73d5cf708946d228726b71ad5e6b1c0503d76650b69b81fa5f993f1e91d9a2447fced0b422a6b31f4ffdd8dfcbcf28f3a74c6854c56b4ed93fe0b8b3e8ce45d5ace4a2ea020d8ab7ec8ff23e6c6450b9463c4f772011a2eca4657d35948903fb72f318672e4ff5062e27c7e4efdd1f14935aae1433425b4593cc245b1a2bf3c119154338917e8a45380658b760f28d836eac852b38e3ed6e9cdabae49de3d7ad9197269dca7a715a65ebc1fb995bdaad15deaf1562a26a7fe3ef1f055fac9045ab04dedda0cfa7d31abb049ad461ace739523609d4dffc5e1f568bd099eaa60a64785134159a5136444eaab22652c7d6a32efec03f12d7dd8fadae2bdc0ae674be764ca69a7e8c4054485a49e0795b67d113389a85437ff9aacd96da1fd41f3dce68c865ac424f45126aa17e9a51d1bcaf4a527a080f0baba0c247b961383ce4fc0ec01221f635387e0a540b40231f533c43f1a5189287e0fcafbe8ecdaaf2eb8531878922141ea7bed74a86f6a0f2662c018a4c8d0a80e00c968fc7a1cf69d53892c354f15a74c420e9f2bc7b2be94a9a456f18664f0f485b98670025bf95a04e7bd334a05d1a8fa51ec898b8844285cc529b2e78be5b388a90c0529b443c27c3c3038da54dfcbf7d3fed9393dee80ad42cb0c9e515f053e5cf15de3af269ad71958a679dc590cec80d1b1a4f8721dcd1286f7c8bedf627706352ab905ce61d15a69f83123e6d38669caf092fd41e0ed4779126bc54e23d07e12a586e835763e25eb4ccabaedd9ea51ebc93ddeabfe0f7d766c069fb7ba1750b7e19d0a0d54b4b424d78e3429293822b691841780625af2c83306e406724daeb3d94e7ed1307eb20118d228a280986c733c74a8d0f2ff9c724f7f7dfca3d94a61e2d280a3dc82ced04b9ce9720ec2df62a9d1f5982fd93e7ad51da3e7be0d5af5b12ca87d1c1ba7e6a9efff6720620d8fa50d67c7803ff6aa7b16362ef789ff07ff8bded35a6da4955bfac897a2c0f64be1133c02da321ba09493f59f139bf979ced66fd2d5a672ff58ead600a6277d1cd16ec73992d264f994a94a7055dc0b717c9b86140f2776aa3b1fbe19e8a786ff53987315f3a58d029a362cf5df986d9cba27afc5407a04b89d769fc975c6ef301c0f1885d5d2f174affb54f6eaab06a0708bfd464e00e37b1a70be159ef4c0970d265d9c14b4cd690a3b9997cbaae0957d3c21cfd7f05ad3a6595067b5cd83a3c04dfd1b0d28d8d14bebfe6b2a6c01515ddbe93bc6f747535688c669f36654d2f5b299c5e8474e9380341ae71b4552f35f2a9778c9bc30e322414c1f311c33879f9a558f3029b3617c2169fc60ed00c10c3303d234743be12e954bf17e21d3633f60c914167bbc6b5d4282bbe77cca5172c03cb1eed886c2375d31eca5c8819311f420b6ac14c44d7f2b93bf0c531418a990eaca6d992c7334aca896982a0f431f90cb36f9ccaf0f17fd2722d2e504dc3f10e90b8593f4fcb7590432649e66dc3fc24e86b65d043c02a4d0ff4c271c637d76577d5433e9b595d2e278271614319b055ccacbf39dda5547d19c72af77e7da667cb15f449562b7c6db797b9d6b28c17619d94b10ba2f3e657e6a418851d9f248c4ddac96cbda5fb1ca5d701d5574dbd2258577e8dd251572bcf4b6f8453e2491fe9358d816af981e6cd47e8490337348ea76164edd01beff4e714c435a40aa7732b4e7d12ef9838db3080446885b3e47d8940691457fa6f476595d920bae09e9b64c35250a327c1be8abb6e17065d612a047144c37d21df6e7e75b69c3ff10c30084390125a2cc73e3e22ac433c735404aa0e16ecd051998476723e5ca545cce2121bd9bb251d378250c6a8ae68d7d37944883fd2780070eddf5463ab2e0a38495f10cf4ca1d235690d3e8c88795754b4467826fb34492e2fef8caa81ef5f3edd869a28a83381a71aee4746f29ce96578a66006f8de5e0154e481b6572e1e03e6745ccde8629caef0943e4a61e1dcac120f8d60917913911caa59d339a4e71aada995c40e5f1433059592ca69793502503356d721dfb910c860e605b2510c0190a9c139a6ddbc759b2e1e56a79d691d2d9537a57d0ce639887353a6315717bfc7587befc29863c407362e47ac9a34af62b9e60f899328d345728a2c2cfd9bf65fc581386bddae272828eb48eb2451f8d23080e3f2b8b7f10207cd3fc172968efd4c45356d014f95498973f99dcbca6835197187d36c93ffced7f9410ceaef152a363d3ae87bf8c15428d84bfa1ce6234ddfd12f8504bf5308b38283aac815cc476b8410685eb86857885835a5b27b782f23a7781d703deaff7bb3333cd2c8547dca91f5509234b0b29e36f2b9d97ff607eb2f9e095880d5a159676ec4597aafbb68826bf8d0aa270ba53fb7a17aa6b33c4e64d1cad986d423d89b22b5ee9496a3eec7542d858b012750200c1b5698d698fac104593c973b3298fea3b8909dc95c0c6a11f8da0c99586757a46f3c005e9f6755256535fd71a6d3ddc5cd9aafbebb3ed007bc97e92f67bf55972568d2176bdc8f6a64925c5215f092c53b3f97e23dac90d84915dfa7cb3d6c17e5d3e2fa9640f49d91d78fa58cf4f9a8f29dc1cbf2ec55b22008382b49492e662e4142d5a4dc285d46975b8a75e1f9dcc4be69c95dc40975e96821bd05b172231e9b780041882cb19d66f7642f872cc238a2f6ef0ca0045ccacbb918b6c206ae2255b6274a8c3d80c3ef587a8f3597561c90c1b7750228aea48bd754d4d9fc6c9a08ed957945dfc2787be732c2ecfddd2bc6c892fd7361e5c624cf4848c37d26206c710f6492486b7a85adac8031d6a107aa82674111b96515e632259f9248b560a0e13cff0c6b4148cc121e5707dcd3960e8aac7d3c597a97caf128c14cf48a270f5c93a101181d8caf88e83d85dba7df514cd35da866fe9cb00b619bb80fed8048e8cf5247ebe77035766f86f4aeb696673f1912abdb413a4249bd678d2918a647520b77f17d916364fa2517feec9ba3698995c8fc5065b54250dd5249060113e85218ad348212307e3f2d58292c23e138e70600c44273656f9f473e4a6cf31628efcfb973debf776c5d11b3801423d93c6da3186df48513dcd56fac894926fc2ba24776f02b2cb78cb620ce2060cbc77f9cee8bb25626416783526adb4249bfd4269ab5ad4f192868bf91b3ee94fb0df22dbc5fed0a70beda9738304b9c8c28cb694e3d936bc102f3c893d98fb9647ff71edcfe4ffd6195dec5a014d99836685faede0c48a80ace154f5886d78bb3cf558722da93245d83e51ff45a02a80db108ff563b141caa341358fe1a1fe7861aa50f7ee55a7e8f5639167b8c2dbb9ef64aeb05d3d1b2d3060ae5da781f531d5a6a0aa788c80dbedcbfa76dad8e3545862380a754bf3c364477074ac58df5e3f17c202a0067874170793bb5c03a21b14833fff4e406f0f2ff99b1134379cfdb0b27b55022809cabf957c14c99774ca124d5a79e990554eff481f088054aff7cb881e7343fac15544598577bd1118828297bdcda29a9caee188bc9ec1638f5e88410d0cad7ca535c011c53ccf7bcf7d045be92e8cf9d780e162b3c36d5e0792849f8b1f41a2a095575b88784e414800cccd04e39b0227185e1777a0cd741e9c9a67c9b72e7464a8e85b4d3439a387d688dc762bc13a9edf75b39ffc950c0d075b9309e2c39c41d00b4e2a6ee6e6cf753c45e76a63bcf3041042b4da99bfa089a51537d68897fd44859784dd838947822fc297cce7bd929cd6fa195f2b406c79d1f3d431067e7d95f6b31276b0229bd8d2c824792bacbf6b6dc79caff7be420c3dfa9a23ab50703d4ed1c0c773e84b0422a979d8ad24cacfb765833f6a974be7c91cb6eb29e926877ca812be2829bc93691f0bbdd5edf8088496bb79d96f9e6c93e7d4b3c982464f582073cc58aed56285fa5e136944f312b76b285a936223e336c19f8740ac26664cd82132301901772c1a38293ea23d5b8c62bccca778a4262c24a2066137081997cd994bef5c22fc5fa5da42f4584b502c49925d2c90ec39dd2a9fb6b54cd68ea602e97e6b471e7ac9b5927d6eba5d10b0ef1309093d75bcb678b17fdf127ac093b1cd515014c7fb725b45b94da9379cbfa3adbc669dc3f24919cb02f9918d947b6303a51b1466a960a00a338d7a1637e0d278ea0505debcdd655ac5d4e51f3136a9f1d46f6f90103424e0988bcf67bd2d0254915f7797a2b23a16d6ed06445a139d18e001b3e8eb9bf3a482fd1eb22f01101a7dda9b09d598f83f0dcffc0229682153ab300ae963334766cdff6b943822b10233f93a5474950193cd6729a8b7fbc1f979705a4640a24bfe4d7517cee5aac9e7f66353cef9fb4275e6b1f2dfb26b3ce3fdae9acd9941857dc8b6c2e565273c8dac4811d55ba6d406916d29ba869b63339b57e4ea1837885ea9038deee3423bd91df0e3025799927237923d2052646d7a658c1ae9b3c8d77cde31e4a4dbd888dc723c5b2a9a4bd36cd78ca9e0213f23501fe003d699da170bc6f36854da259cb3be835a57c1348ea93cf09703da8e3fc8664bcfaf10020c5f2d4363b19f1403a6c15a2a08513b2b63ccc56b511760c5901b04c4de5f47e9e50180a6a8363786bc8152423577564a0c5bee3f6ad4facdf41ddfcf003ec4da3064796afc07dbf7a5ea87f7ae8946be977db1b96a1fc3f853e697fb5f7d5eed4e9b04bb3ba49afe7334b9bd4739f10a15db2f0b9e87173c4c83ecdbf0f72bad49fdd4a326621f22b6338dd735dfa7e2d8cb3c4ce7710828b972c62c54a2a9e03a2aa6827686202597c4f9678f5a11f9afd7b83971feebe920ca34500f8e843c69ca20b16b04a9bc7a558007ed1123c471da4837dc1692f169b44fefec0198c3d4603eaf22f3e04c0e7bbe66ed82fc85f5c49eadb06c07359754ed79a527a92de8d9bd376912225438a52c3ae699db1594fb098ea13b7a63426eed81dc537c808d9ca2ef62236e24ff2d4b15e8f6a3dca747f6e8a9a258d4f0eaee3858821559dad680970991a023c2c907f33f1bac0e37f32d92870129279068bacd45bf17b5eb4ff5c4146a5b266203c1eb0607217c16e9f43a3ca5597d2f6df9c11dab840812e1e8525c905ddc00d5aa8c763a32e2e886383a29c6b6b676a856669abd0b74bb17e0cd8d0376d6bd70303688c2fe49987503fda5508fd0a91c1a6970aa1f96c9374ecea51c25a119e908294c077e62c6dfb18f5331cfb6b5cf237c1a407b70dc452b9cb3809c1bf0bca2308d8f9455bda05df654482eae92f48c102b4978b6858506b306a1abee0cd12eed913a7e0fbd1fe015a202bafed58aca6beb3a90a6f59491ed80ad62f01d4e7308737a828e1e0d971929f45fa9c0a3623e9ebde489d337552a8186795701a1e03f685083c8824c813fe7d1913433d12ae283b017a3e47914a96f39a41f1065f86ae53cc2cde602168857c8a563713810d4bd89dd84fd09749189e787c0506ead8576b2d400a592c023887a7bdc6044426b79e8bfa81bd3a25b5d95396efbc70238b920fcb9a92ea9e5c6236fc27909c77e8bd3281d00b9a33ed80d2a24cce9c40c7c7f005b8fe9c921e6b76f0790628d6d4ceacfaff5fe6d60cd0227c653ab6159f4ab11714a9e146da6ce6592a36a025d4aa971b8c97f90725ee44ea3d02a3650815afffd4f06ae52ec452b857d77e7f2f3e5b8c4381bc35e5a58593b547b7d5d1174e15167374076f9d99d514b80a503e2af3134922eeec2a37cbafe2b1571e8db3ba7a74b14968e1d61ccc59b7725c6f78aa1c316fdd9637d285ba242b7210ca6fc8ff797c7f3f57cd55df599f2217edc9f8cc56826ab331a32896ce6e8cabd80eacb29f56f32a03292daf6bd7cca279ba5f8fcf296ad8f115c094542d98d4bbbcbbd34819727cbbfb541b3b61dcc16b00906231a1731700af798370033f54a2cb7ef538f0deacbd4cd21a5754445c146079b392a7f237215fa98c02ed950f3ced41238ba836ddc36632e6ca28bc64b283bcffbce0e22deab18dea6b053c9f407b567cd30d38146e22e971eb921951f046e3dddb5df2cb9653b2ddec8c94907f8af054108f503a7cc6cf5b3435a35253f08739b6157dca4b70ae098e86ecb711c1e2216656acbc4eeccc45801b6c8529fbcee0a0af0333a9b47082c12a716c2f64373d694293b81b0d7a1f41bee3f912553662e221c5939b9a5d62989df0e32b0e409007ee10333088a881941a631a0ff5462ce7ed9093610c6be683e520d98dc6bd0e870efa1637b1a343b8ff36682102e995bacd302fbb3dc4a43d5da4d2885a0c78fe975d02c33eedb1856944ced878ebe3cba557c8e1f0b7faa6c1843c4024ff49cb50363b0cf7a08544492c2a1a20cc82af8cc11ba34ab7df47b9b49ee2e08ea2d6982617970627334793644447fd14f4da43d5cdecd95fe7ebb75d24158e694f0f6a27b373a3cc9ef8f7e09f5bef70be0dc9ee633200b7f477d2d96dce273517a6252a92bbfd14b6caccef9c351ae6d21a9b84089c68564e9ceeaa7ed2d32fea1dc9977d97a3af7b529ce7cb067706995c22418db9f1681e93356e978251d698bfa679200cc20993b67a829e2d4cffe37b2325eb4891a1c6a6966a3e4f28f8a388d9c5413ec33376a4a784684f86d204d3f3b1e5219f79e8418f538cc4dade7fc27644724fbf03e46bee562ecbd32140703ec583f55d11ee126cbbfb3f3c64b549e85850777391d933c42c299f091d4a0161e4474d4574ec2f13fa235e01e267d6b6e56fe85ea10015620ce77a8a896e1b0d7bae900e8d691974673930a6a9cd4538ae25bba4f0aa0bfcaed52edd2fa34567178c4dac1f1a3d7054ebd46de748d3e95acad7f6ac64664bdf86a4ab25765c44928b6e9c851ed884d5cc9ea166c8a7615565d9ebcad6f7bf1e4e43b029b0f3c185f15f78f71a6a936a4dcea1101622bbae0aa574c5d60549af11faedaf2cdf0673eafb6b83ad902577d70266a61357cdb1c3a405bf2e0611b82bc8366637c77f21586267e59e32cd3fbac70800c9518cbcde0a0b84a7a282ff6f7d25a558ebd2aa86ae8e9c2f56b96489132ea4eca8edff81f9daad0280380dca37b4f929d2391895e836b5ad82e30effab694863a590b3a92dc1d80024545e9b4c30e0be7b7cf4cc8f6069b96b92a551cef08f16d867aab75597588d208dcddc4050fee5bd4eb237801fb422bf295bd6b95a0c12b1da56d1b410c8decf2101bc4d51234235a2084c994e5fd8e46ea4c0d2ecfe163e24cd6673af8c5f96b3c2c9c11f75c578fb018ede34009ceb8b02a6eca7abe3e9aaf99e3386693b7ac546ee02c3f65bb39350f54a956894698765c6602fb284ba1123991323b3bcd981917db3160dbed17c03f21a26691747f878a6dc0feabb59ffbceb0b6c7e4c38ebe7dfd81c63b4909aa11ed71b6f9c45fba47e5a431fc2a93ec5bb18cc7f966b5dee5127517e06e5e91cf2911b97fde06672895b76fbf5e74df59541f0ebdaa0c895ff2d87a7ce3187d2c684f71d9893409b13a2def940f22b76988f911129ac2499ed5e0fc7c2c12333332df31a904ae39300eba6548cdcaf11d86c2c0cfb3a29411317636d411a8a70c6b8b8b11ed54d5820edaba812267193a57e46434b9795614c57aba1f82bece57846dad2b737b1c4ef1187d0b03750167457389e830a44dd9f04ce6a2265016f29c4ba1424231350024f78a83b60ac7d3e376597521a6b9f3d3b0465b26beece990baac0449d50ac9b21f6f27cabdb315e4a46c160204c33a7f5308a7c60689fa7ff445ec72c36777f8ed23e477651d18a8e967efab518ec3feb7a87dc5a697ccb63068ce99ad2bf9ff7ffb3331339d1309e91c022eb8862c8d762e4fd3ae8149397daef7d798dd0afd275b65143b7866539993c1c5b70422bf93156ecbd9f17174b1efe7ff53c920a20594aa8bf38a0acd94c1eeb54967af334be6e4ffc346cd4875c5fbcc654bcfb35093e2230501f8dcb69165f1589221c1707728d6e2b4b2b3c6fc2ce945283e9d6fd6384655ec4a131f37c4a598c60384b381bf5c4b173a1a1113afe27ad3262d26eb47c64242949d877f2100c1b4235a817af056c38b7580caa09f57f56b58e9d2554287f1f4e9d4965786e6cff06ae50073bb4196e7f5e9aad6142361f264d14117e63b80f61b73cc0e5e3bf961361cfba87a2ebf17851192be8bcb2a392f483f2931c19ea0aa1917304a3ce4eac3d5a5bcd859ccfb088a0b06ce4d9f465427418e5c3972e4bc6125daaf5df00f91f10d49172fe1f2a39d01816549aebbdf843db9d6c9a466e7ebca9f0fc2f5a1b5d2214348cd3ba7d4644f874fa13b90312903c17c24e98612c7780214cec7182178f4edc51a78c5ebd58ad278ae316924e6ad8344351317269b0a0a1bf0143b73aa55152cc692438c2316b3a65bce567abeb33f1397ddcf18f65b724f806de316c379557f70ef489115552fd275082dc7fc1c190cd1a15c920df65d3aa88d52cc19f53c3e902be25f15af6bb527c10515ecebfd77e9ba77d986cf3dc3dd5a994b191cefcdddd6ce6f643257f95812fbbe06018455e4fd932b67ae4a6c973a0681f9a85ecfe7f4ffeda43c10042ff6bc7895df5a5e3f0f29ce4cb5af716791fa14bf228737bf3d0d6f3697c243e91ccbb7c4eb924d7181921a9d1bfb1430adf31ee6d976a1c673a7a24a26ecf40626d3244696f23b989c0643675266cd2928ab03e0f35841e20aed541b7d2768489ae16effe5708687451197c44ba5503ecf373d9648e94ed6e6ce0c1de4d67e5241d5ec50d744012bd29b4c0e59e85cbb4f32cb34e129eb7c21681abb8c4c6301f3c653655dbc27c003953886c5e8c1d94c7c8f0f0c291f10bf84b284f83ce432925933eec372ecf589fba1faec72e42a262cff5a89c8a26cc601cc651912fc4af6e03bef8b9763f77244de63362a49dcbdc90e98de73ddb14cded69b558df11617b27be0d8158db33713bd64777fd54908de5ffb1598be17fbe5f8f4babc5c58cd0ef61f1784667767c938a80b72d17dfa4da10b24afb4421cce7b809baa45482608f3d8915ea6edcbb835b9d274b77fb44a2379691d448a18a853e57c3c759735e5e766bac6ca52a5eb61b56db5ebdf24f868777f12141efceda276acd9885cefa91bbd2abd292169e2ef0b441160b8ccb4111acef20cb2b5b96a8fe6c4d1e117572775baba5894a0b6bdc9401d8744d55a0fcbfeb08cbc20f7780a53d13158270a5a2cce66e1512577f49917ac074c27b250a0295d88fdc5954d2c0ea1e209e4220377790acf1849ea5472c74cebef5550b651350b4734a8db8c18d46624755ba0ead98e99ad600bd25fafe7ed8a96f786fb66da8bd78df4c09ca29099d4882dbd4e5a5b381f6aa386224e0f6f9a27bff7fd3cde013104d73023141eea0ed77fde412a53d6d999e58e9a902f96a51a8865a68de2d8398385715b8e8c7aa2d11afdb48cb1aed8948f8ce541f7bbf5fcac73e4c8d4ebdb3b13fbf8b4cb6d444a88d13382f7bfc7fd07c6d71791c9dd8af8dd39da10cd20f5e1b1e945853d2eed853cfd6251d83859e93bccf93ecefb4e5759cb6dc842c68028dfaae2a70a70600275d77d58208eae229ac1abda0fecf26024ec432c7b81f09f22d780e70de4bfd3bd3c0cf861fc4847bd48c2d3b4dec9396fc68c368c6259ebff3d04c60f9a825d842ab23eeb9efcad0caefdba11a270a8ef89e6a7a5739fa4e0d6a781e7e3f9827c7de1f4bf10aaf807033a2345e882ac4b0470780973710bb6d29d7c95fa2a951a7f72ce459e2353807a27c6bc47c2fdc2e367f8555840eb9618088e3e99dab1f4d521dc5524b4f09df7f59767f9857b324250fc82ef5b690a09a7abf3f21a657b537f56d1c349dab5bae1cc9fdd55f4d2bc725dbd34118b1934e23e469fae8a0678538ed4066adbab481caaac76017ce185af61c62c37a856efa70e54a63c26b1021874c18bf63c9dd2c65ec1ab51b9765d9cefb6a6c885f5b021f9abeb8b628a69f04a379067738bb376b58ec46a2110520c126393b69da92df202b537fd0d90411912a32d4f5ee713c0abdf5987d6ce80e48c91b5446cfbff77fc9258414f51a198b75e4ede2ce5d214a16a767fa483c31f9954702582848a88248d7daff7c7a509e41407e73a11f7656eeb4006640ec42d959603b9330c4c40d459a3e5ccb36aed870c9c0ea444b97fb14290cde5cac0dcf3d089be9c928da4d20b7a54836b0a8010c5527ed334139b2f5dcb8ed59d86599a18a6c427d8edd39f13349e4bb1cec3785ec7b3c26b4bd5ecde8212e2216d4606ed64b18238691b243b8b9514fb4c77582bcb610b5fbdb50efa05b7788e58166e28236d9b11b28c120146ac2c2e5f6f429d6d57b488cc192763eee28bf6fe1f12c7682f183662382e3d5b7dbe96ba54ab5fa9d3633004ddc2875f7b44225a76d0fbf32fb09c53bdb9cec6c12ee5ce068c76b5174f97076b6d91a2bb93c57886aeb38bf4faa87f2c2b21adb4f0d1b76b1cdafb75d90eccfcb5f2f3152d5c751e6e6713f4bb17cadf715ea56c7a524eaa4d29b98e56712a1af96a14313f461ca1438fad8d34194031d974d896d4db892a98c107fd12755c49446f00c13103fc68cc04a36e9eaa937dc483b32fce9e9011e7d74ba0b169af98316414c3dd4ce0f1902b21d3a3415041828a37c62fd0c0515f9778f400c905fbc50a0c339ceb586bd079ddf2637ff8ed2b69e082cf9c8746760947da3243057cad9146862f257c4f8898c8f01aa5c2134ed2f026444bf7344cb541d3d80d78095caac3be492519547abea9073ed0b0c93b360705338a875cb196c6a9e1ceaaf6a3f51b81f989d4debeb1415a7486a509b0f62e56ca4a1735c5022e51199abd992ed731733586518d4e20983fad508ee43b7f97d83bc36b23b01c086f7b9616049eef57710c2087a765371e0f97c7299c63e7cd7bbc4a2bbd5eabc0da199109c23b14f37388bca75face097d86252614d8cd8bd29abf5033b2e011e0a5e57c510c91fe779d2f280b49917877bb4a8a84707ce0670f51ae1fb91fe8a2a833a94ca34c4f7c6b9037561cd50a2c727c983c5c0c1c82dd960b73656803ab8e05f118b0b5efb622d9be9b2f37dd69fc96f5baa408a95d1035af82e283e1d6c56acac318c7a083dc2f68eac51e1c1a7f833152ac223ba7041c4a026d11fb3db6f8957bf5ffa9e6791708f42b058cd5733f7e83a7ad7097b1bbc5ddeaca5ac5b8fe8d859a4bcf79a5dd251357f06fcce7888352c12dc7e15fa223f6e2be141f362aa4a74e5822239cbfa5b5f675e7bddd81a54864043560579102e01eacaa399657d9206cd1321ddc8a4f1d8e0822548bc5882ba8f29bf9e4720a7d361b592792e9c85f629c5a6cdcd0c0e1d2654901d8ed50a7e8db92f82778f23171b5117a46a6fc8933b1c83c7a23107e19e2144bf450f7665840bf3f25d74550af23894691ce062c2a2e545f0485a088e00d3ebabeb356dd92ca910e0063ad1cc4c351b58fe5b6ec14272be75c8dbf1f29ab4b2d025032382381059df7b90bd4934ed92533b6a2f09d59eeb3a751bdf510feb799c492b169faf0b66d86fac50a92e7babd34667ad81b9e65c415bb5c2a8a49f484c4fe15c2d213b2664223cddb65024f07c58fa0afbcb9fae0b0ec36f9d9bb176d0c9c261fbc4242f87a40cbddd71afef46312920ceebeefec038775ac15eee6ae4719e2e44166633ab64084586fa7a2096442173dcf2962f4782a00e8fff697d7cb2143637e8596ba0686947057b12e02d0b4ae4786c81e938e5fed4f4a9dbfecf17ca15c95cbce1fe48a91c4e379f334e2a7661d1eb7d832a10f37b7678ce901618e2ab369719c2483925a7cf06ea992a4cf638e4f3c471d834ea23a8d6d1edf6ca41e91c5361337926dfaa384ff4e552be31c718febe993e4f356d277612e4c91564cef3df4630e4909078a89f5c95150849be6f0bff8a5b6daa83a7fa2c5393063717d0ceff0673a677a3859dd54edeb97eddf1dacd37b924f756951a95775f2955430b9e8e73b080e50872ab41d8c4d9da4232f6a225c29a941e18d0525ad12caac4b4786ed7a168e16b8ba6491217cbf957a307cc11b665c3a23d49eb7b4ada4f5b159e43d089ad9fdba1d2bb014a4de279c7ad0d29d36fbb9120fee4de033e6f5a02db444d3be2146b678cb775ebf4a46a44f4e4f9448607c73829c3010d3439bf751d10db88ba385a8b2b53282c7222aa87b83422eef6f7e24cd99007b1261cc080b5e2f68ec20ce71963b86553a62fc8eaebdbf3282bf06282b3fea2a55326632c96262e79985ebf4007355721ff3ddefa0c1e0e81943315adedd05f9ea266407a691ec1410773bcd68acbe72909be39c54e120482498ede32511894d8d0cd6b4ab35a54c19e8a4092bd1c1bad49982c2a0cf1bc00d617c829342933442405e8040950832674828075de7b676fee656b23b7fd00795679f90763b822ef6e462ced0645484c65ddb3c9b7d66dd788b46a7dce3e0cf465a66fd9483fa265ac9721bac4baeeb4cf2d9073a800b2d9db706b7b63b3d0ee533250e5cb9233a14712feb94e5f74a5bb1875866da40a80fcf1278439bc6bc9acd2d30171c86ea87cc8d2e9d25837ee4ba6949662c5380d84408531315165fd440700504c669cf79b510159b8c496d66f34eb06c7e7258152302b0ebe54258f4d6a7c642599af6a6979da37eade9d821b4b840842611ab42bac1f9968cd0443cd4ec40637e74c8641a09b19ef4a9e6572ec7cfc498374447c9049fe1883b584d0e55974ac7aacce9c775a261a1ce1f9fba51b274ec395d009aff9b9e1aa7bb0057c5e586277a6d12791d987ca7d073a036c7def447941ed72e2ec585990bcafca5bfad28b443b0c59efda11d024913c8ee4c15752b6556da19ea400aba2bf49e2a9831ba15acee538e5536c0e93fd5cf7cd3744fd2656a53ce7bb6ccce56d50514374a2f3b299a1dba2b56faa654f915af7ba595dc7efc97bad857f214386cdccdb7818eeb9923aa60acf7de9c9583dc8f7a8b80bc458adc3c6492517c42857005082425b758912cbcfe6149e6631452528f4ab896856526fd8965cae2cd81823e762f5c4a9e0cc8d0f44b511cd85b443f2cf1bb7a43ff075ffda2865a42d95004ee88f065a90d8c632ad7592d7fb3379dd45f10337a67ad57be77bd653fe446364101f464cda643f8510034ceb672e9d2ebc2e263bcaef71b9345842c99cd08cb72125450eee672258608018fb0e1b24beb9aa5391b9171ce18a5bc1e3c2be2f601aa06a685bee03fbbba290d21709214a4fe3b8ebe6cfc518946280ab6e4a82c90af7987438acfc84a233c55799e1b90d8e15d4bfdce3ae4852b18edec8f60417adbef59493e90752556cfe06220052ba9edb02f50a3a6b7428536fc4d595dbcf4353bb96de52cc4ffef34a07d1ee48a0d30e91ea1499bac5c91cb09d89ae431a651a6144b7869883ca344a87bc42281007226a121af593ab204701fa804df60f95bf09885a9d0f1fe23461c089a4f70cd88f8acfe2b76d8f259b7cc9bd1e9f7b90afa078c10c4ac6f160187a54a1dee1c13948b6276a0dfec43c23b2be5bd47b8e24547b2b2352c38089e2135de48ccf14785b0ac485418e14be84779d13c65a79aa34fffe7cd262fc84d81868c221d3cc8243fd60262b2d245be413826fe9c073b9ad971b446ac7124c6a06894765208d77a6a1f3e8dcd180495ab8aeb0a5009ad5721eeeda669f714d84ab02e0201a7bbfb7d9bb03909969219f19da1abf8da76178f90f1c1a4799c05eef9e43daaa8c188e12ae7cd015101ac801a8706ec46ce63c02b2de670f200ca2d211fa153abafb50ac2d30d1b72447271515b28d4c1f169da1dc03b052acadf2cebf4d3f6f4e6a2ac5798723a3ae0f3da7ed5255882427b29fbdba7720c2018f4cb639e69fba6bab35880aba86549f07c69c7b752d1eccbbf9d3204d3fac55ac76799e44759d5fe446cd4b3a0a9be8218dcaf12875a6560c9d0ce66e72dd8e2a45f14ecd86b8e378ad52ad0ba93a7d71ec904b767ceb64310e3b44b42d00d326b8152ee3d97fdb03b2ec9980da335eb9b12c08e49992d6cca459a9e1008690fab061f695f5fb875eaa657d691c9d75ff1996687b6c07e3842e8c2d97ce49428b0fd8c157f0157dda59f019db0644a3723f51a72b8dea0d19a1ceff84c6f193213c8eb2bed4b3dcc4e2c9c4ecd3d26239794252145b1bc2b7e6c0409d155102adefdf707df62d5607b7c8cb3bc1dc9386ddcc7363417a1fd34dc8085dc55eee690ec61a84bf7da898bd4b2e396958f6a0b445876734b3978973c46f6b65abc4be492ca5685454293b497fef32a1b8b2dc48262e216f27377980e7d0c949e6dadee1b06e01b486d3e0b0a81528a0df3c07c0be3329a51846ebb612bb8ef98d2a868eea6f360f560eb4bc8a9d49355926910fe42f5700affa9043dca580f5b2be8be7e422866f25206f67aa4a966e493227138bdaa7310fe2d28951f7d34ac273e77481ce67a85eff832cabf3fac781be3bd1ce29b58ea983cc78076bd345e4ac8962d4f402b2b67099ffc7d265d67741336d395aafc339b501c89b9e810f8ef287289ed528cbde9377b301ba3e723c1c0b9d1c43fa3918c8d4c6a8c2a11b39d85eb541ba356b43007faf590f2f80ffc8ab437b0ab541adcdf3ebb8a3de08915d03df35324c43cc6eb9bf286352ac6843fe45194917c116e3e8f2751663ce95377b1aea6b153cd28e76b7e5a1e0df20d8c8f02ff94c302293e191ab665068e80c2669272aae13d6f9f519f029528b088a31c21711860eccc7b670c84801e325546277765f9919eb3409a3c39e33d92bf1f675f5a7c247b86bc05c219bb4afa0031fc2e9ddee346664199fcb9374fe53e606334a5f7727a815d6e1fbef8cd2c80e6ba1082b48094a3a8f8200b9cced2dd0d04aeb3f420ee31935cc397bdb94802f67e10f7b4554212a9e8705883b3d8a1736436750538b0d0756330f4102fd0a1099729fd7ab85e2fa205e33a62d19a28544a3cc640b81ad384b736294d134a79558ae1170c5fa59635586985a96f9ce6d59c55c50544a7ee3ea0f371a85c460215296a376925030d382d40e99a6d3bf4e83847a860faf684a4cbd35ecd0c222cf0e952bd7c6dd2b2fb07d5d6b95f141c8dc04cccde656bc0b1c8f9ecaf849e332d69c90c0a8eb40cc1a7d107194dd472014939455f0bc30e1d7a8a0da0a0f79013c92fb0840f498f74559f6379f4eff6664accc881cdc07d8cad4e81d72bfde08d8efefad734cf83400fb750499ec255fe99958f233dfaa9c61d1595154678795493484472055ca3a4b41b533202117a15ffc96076c910cdf1fda09aa1917acb2b3e036b2ed2fe632e76426816156915d8436f48ba2bd84a201aadfa2d9381314c6e5a35be790a12902e0208ffab75fbdf0da9c764749ae3fba3de379bb4f0207ca0a07b22ed728cbf475dfbbebb89ca3052766717f88d6a5c715a47c5ccf8aa9c754e7508a341efeaa833b6dd671a07ebd26c40f70cbf87e083f43a06e06867d38071ec2f155a7a3dc0bb6138824995e9af117ef127ec21ad27a70fec0bf9fa8e760915b6115894187eb7db541fe3f98f1aefe243162221610b16587b804a814a4ae20e4bc06c047f11ee6e66d218c482ed55c3217aa45e16acd1c0a6faff1ca9ab1f2aa267f8962ea2fc8aa4453a9860d18f0b39198161ddf8f1ac037655d818b65cd88c8ee56c9d1e21f1922090fbbcb3274051f51f76c0437d679612e7ca671219bfb435f35c68abf0ccb18f6a38c65285362dd2d15949d7b0e221f2190a790d30521135039d955970f3be6bec40a387d07873ff6c5f97409f19fe66d6ffe900d22a45f2ce9ebce456b8eac190028acd03f58a90b5165c9aa5c40643116fa92b25d4b626513929dba631e1c5ca415baf4c2efdc37bed36fd3953299573ddd43853be513042871096222815c80b8cab3dcff9334ecb29a772630fc0f3f8b1c8fbb0a880b0a29561a2a5d77c816b5857619ca66644094805507a58d1e113ceb3074e5a3eff63506d30cbb558083413c5f4fcbafcabfa81a3e61226a7dc1058e5ecc3f4b2ceb072836354d99b2d86473981b3864eb0e5dd45e9e64d68c1602881666c7c29d845c8ecced749b1d9e24e1b7dd8ba4fba404ef9d4f542ff8690af0be83dbcd45459ddbec32516fd69e8d3a3a40cd26eeadf6f41f53bd181da2645f38929615454d9990e0f75911372fc5fa7650979ca9b2750468543fe9664647ed315ba82857aef488dd4118bad42c1e520f38388d8568493aa22789b305a9cb774b2ef718574b6ce265c9b9d32c391aa440ed7451250089283b90e24fe724c67f3b27637f279c17699aef8812c66c9fde0bf92f1c9468cce13231e6db50aa3b9efb8878cf883b13eadf83119ca2b2d5b823628d14d22634fd16a1c749bed0738ecd9ae6ab0d752b18dfbc2f276ef96cb51a198a98a8833fde316c9670d6058a8ebafaa062db3696e557cbecb18a21588cfb37a052e62a5a9a52e22f590f1afe30e397283b8f19f65d3d9e47d950a273fafbd15bad0efab744ec615c0335ad5f0f55d2d7e7c60a920c0d1f1416bdf80312dd279211e5057ad4348e00edd8985aa4b402efecff56e5d02a4d818c870a24fd9b1d8a1bf091754b777060591e1ce610910a23cd2f410cc273a80d787b69113bb29996f7a966dcab72a59ca1a88fd6dcaed08a20a1f16e5d6d371f1c9d8d2a40bfc734cf42dacad61000bf8ac60454dd2a290f1d66c7822f7ad1187b5cc7e5e30ec80cf7ca2b11e35a0fe30ad8dd654cc7055ac868475a4ea73d7c3a4dc056c034ea22b2dcb38fbc52e0fc5034e20261cf051817f5b5e282b53b61924adbdf0629f6169827941a3439900e38192291a838eb86f21e596f8fa749c4cbb1cccc2ec35d47e9f7054ea71b0e442011cadda8009fc125b72d8555fd6a04372d35655647177b7e99e5a8b332354618cb17fbbc5868f7210f02a3eb774bda7e33a99af1554fa00b9b371d3477f4e927c126195f8eed63391b5a69e2a5f51e2156ddadabc9b746466b8c084d7cc8a6197dbf457e05092c400abe67412f240aaa56211a0fd026e642288695b7dc313ae35a016da9dcc40c2cd197f7df3f8a5d3f8a17f0d0bff1fd047ac7acb48edc97ca161da0fcc6fb4087571a0025092ed9d3fe2a785065abc2d84e94c5b4fffb5920361daa741557f79706e51acbe35cb8291921589448e983bf4e62e0ed9d49342e6db9a20d104fd0de921b0796b5c66b7704824412c28fed908f7bccfe6fcb72a01c72c9db326c83d825698583bee00fe663329f878dec247ae940b16f40492b3b9406c42766bc7ee726fe222df65366719bb00e67a15ec23aee24006680218ed3883d5a8aab731fedec8f8b61ab121d708728625e2c4228ad6ba668b6056af6bc1703797016160eb8fbef956cf086879d23e45c8f5784ba75d76957d4b19d6ad333812758d5ea92eca12628eb76246949b11bf7cae375b26567b5179cfdad6ba38c9111470fff87ed260ccf2d9702b2b9683c35497f87ba13c6b68f002345d0d6457435f3feea4672234834a7f0e94c9d29b0e3ee8eafe19e88f33f79a5377ac6032ef88b357dcc6e39205f6019fa61d8c48bc1193ccab0dbf3deb624a09ca2fe6130f6cf5b075b94f832fd64e3d872d3330e2cb0c4d54269a8f2c656359987f37cbf83943428f0c091c6b57b088d08df0f8f32d4fba7ee9cdc284bc2dd821100161c875b8c46588f70d2c9bf96e8cd6d7b02541bd41abca0e0ef472c4f5e3b931cf0a3a0632ff17d28b499be4b40956ad8a1172ad5052da80f364270242443a1eded3e8c8ee642b656d0f2b2c68e7cd139a00c279fbedd7808d869ed40305e3052fd1939ad86f185b7f947ea782afca44d1be82cb551006768dadbbf59341bcfd965350f9cafc057733f6bd1651d3fe44f19ef81ada18ce596978f2db81e37be42d092f010a5f65f89c908ffae749be5bfe67226af9b6cabaa7ef1ca407eca3a06926d031a7e254fbab42ea422ee4be2237f175cfeca46752dcaab87b2461109c63d5e01cb8df0605ef508c31ef6067f02428d8b7256b7fa32a43e5d3c12d72dd307190dfed9711addae190b044f4f343a76d1a3a9e7ea3aff05083b1dc9ad6e2b5972163278233112d9bc9577a298882fbbb9dc8346c844b0fad95278c965e3c4ff33ce307d47519b0bdd83d4f7774ffc2819432ad269c001c44e5d1467c1ab84401c73974b57e2de46032cda975d25b8ae398fb029507e1be9a793fda65bcefd0b0a15f36e1a9b97898830bf65e216bbed6e1a658378e7024da62c109c928c2d0a1e4a5b7715189fdffed5e2977d4836d653dd0199c42399e0eb5d6cbfa6dff4105f1170381cc42ef008336ec8eaa2c960af796d0bb38084f2d24257a1b5132dbd1f56a27715a1fcc3f1ebaa05f5443dba696ca0e700a2417284cbe08243d658c12874b34d57e20549dd71cfa402edc9817527f6c546dcdaf97094840ee523f47b72a2732217aac3dc34b2c3c08cc61824c2aaef925e86370ea5753def7faca911c0921425a52753cc4fd50a109ba6b69b3b5d81276452b72c0bbdec9b4018c56e4ee5a93692a272ceedbed38d5eae54bcabca93e0366097d735f9643ce605e4ca84b84ce08529123ac5ce0fbc36d82220d5eff3e3bde10545b93e6cfb75f9643db2cabbd9dfec7424f9898603c857c65b70ec6c30c91b187370d0299638921c9fd2be38e72d3e0e4a4bfa65e1c9b6895117c7b6f9051336a2f6283779b8ca2d3b9267a2ac58bdb48a1b47921c2528715e736e2e2061964f8423f4357943da7aec4634368441813c5ace4b6e8ab53f0eea91b68c6fb58fa12987cc392d4de74b140e4ed6914b10f2bd9ebc5bd9d139d5d1c407c31a8b06a011985d9231d0aff2ca629b017adb3659d1a953007f76914589f6b614935d2f155efc88b57ffe1b8c5e6acf02f84c5fa69811d5011781ce586a512b36ee840a29cb511bf0d678f2c0413bf80a9e4387b47134958de322b7198de90e1dc0c23fbc5884f1c5b6a5488f8ffa37602432d2701f50cdb25c18f6206e0e6fbe9fec7e0ade95bd41169cf2de7a3a2fcda36e94faf3aba3387ee3a71ce2281e1c207f6876ee6755fafaaadc203c0f7f29db2eb3816b7745c11c4e07316bcdffe8273a33d79c38cb1d34201bb34a130e95eb13cf2046dbebb2aa789f1da473cdb5cb178f2865dfea27cb9486a3398b9ae6028dac673679663b0a843e8dbdb411b5b85033b8fd7fbe60a1939832c678a74497d89b987fc76255f78c00daf84b00275c7c3c849ef73d69b6e1f10f091eafaae4d2381478bcfbf450cfd191567ec9ab83ae542d41c545c273f3a9d0456a52ba177fc3173698e4cc03fb6d8a4fe536835812bb4f86dfd9f5fef181ad4905e92a7d1ab9b27d54acaf7e3d09f854b54b8d123438f9979c995537eab5b15f802cdfe1b1fbe4e0e72450944424a6855119d47172d306e5f215bf25dac96dc642d6f9f9822cc9ee3f72ed552b68535c6731a68073d8090ed2d511fc8872f30c17672779b3494e630a851466e48c54f235fd526e4385cbf7fddfa6d73172e643f8fe22d37c976a2e5292aaf5a35c7344b0eae53f4b4857cb4bfa82964b22cf56621dfed2bc84140bb7646c35caaeb7483e7faa8919eae4c2d9e04c739624731f5e9293ce3c26d1714d69e1b53f1fd7b8b578b9e37316ebe7c21bd7e768c5f601943eeb8c8cfc1a71a41a3099714147c13709828a424363c643e34e388014aa2a2e75bda1b591bc6caba4d81c4548ed7db96c34a08af645b505d33fd1d0f7207443cdcd122bed8117490320a0ebf707c03380d7edd282be3ecfa1f615fe4b8ab902afbb8994026b0f2bf73447f3758a0e1b36faf77cf1b2ecc9847aca51588b170533c9360fd4c9cb085e4a8baeb6c0f7da0be78fada38815dd588479efe276232b64e997607f4b5330337f11090647116584ca4f9aa7a36535ab1254e75494f2c475da95ea4e18f30b7a1986ece236b0cba917eae40c488d625e19da533e60749498f71f10ff7d930aa4694aa3f711083bbc19f84f0f75d8126f87c1c4ba64ac0403dc664fbc98ae5f7adbb1db32f555f1c9b78416f10c979a9d2cb514d48ed645effb323344125772b395b24e17dee9f59be8d170b59caf7622c736dc7623035338aaf74e42a299420b7abd43eb430d027277e61a3ffd35c3ee5124a011e000d0fc7326fbe56fb44f0e5d32c52202d56268535d2b8fd924ad42a0160e8cdab9e7b234fedbeb45ee3a98f0160ee5f8773b94e066ed230b9793cde722823be47f02ff1da63751350ee9a735b25b598a7a883f0531fc8bfee9c266c6ecda5545daa36c43d665856bfc224f880cadfda0cc0a3fdbc8ccd25762a3ff6579911aaf1a2ce7cc997e9fb0b35edec270927402c22fa4f82b60e40073fa74b2f0b72adedb378a61994204b6a5a2961d71ebcdadbf02b204dd66cab17e33183562eb89a71d7a78a98a063d4c8c78d5e68829bcb8c9e07c5b39fcb942b68342089d49220a2be7759e84bfeb3d7d2cec031622e0c35d24fe2eb36bc2faed02664a3b29ce48f55250dcb00403e335b380b39ead1f0c0e6f09e79a048556ffab2ab04a04978b52c82c284f6811ded9ee47a7a9ab819122d244bf414d1d137b5a93153bf1ea0339d5c335d6ec709a52b4d96f0ad70a450360efbc26cf9a961a70659de3fa969edde73fce0568f2ea4cbd8c93d9890935dd3ed51a843e7dd6d588a4ee86b0112f56507dacedaa436b0d64a0d9f14fd58d10d1a8dedec95582acff21680e8ed2eeb2507cfe776f68c3cbc88365a55bd83c85cb142c0e82318a4db19f596a2105cbdc714e2a6bdcd0991270999c972b21f0364bff493f426658bb211240ad4035862e5b3bc28a6cb95f9abd3fa01bf45e75baafe185c5e3f952b96fa53f6a698c682fef8f2ff197b5f55827e90d16db77b79804ca4f52f60e29276682541959e16ee1d264e0ade96626d9c0f56d4b76e55109366a0a42ed2ce6f5e8a7b0b344c536fca99adaaa70de1e289e362511efa8ae1dfb457da65bec59f38ca207b4168a7afec3acbf34ae2870f97079271d1f9cb905924293971f3aff9c5330a3335e916696c5799c93678b1331b771f86c14cdf991a20d35e66cccd30b1be2330d562663c64db717892df64985560aee8f454a22313d0d7d2e32a31dc132ce951043e209a0409d388feb0905bf9b9f5c0b9dd6626162194413dad059e4beb575d623574be929b3aacc7e1bcb0554aaace14ce2d033b9d3acb7002f44ef716c453067b470793e0cb2b73657525120f1379a6331168651f6ebc7f8cb5cea6a33ac938aaf800ca64c9ff25f4aab25fc7d07cc167424dc10105b059086b8570596b2faed9b10cef569183aced3c705da11d9fdbf8a3fdf5dc7b9aff52041e66dd1046ba21c3067fe83d6a257d3f02f508165f35e07da9a87109d8ca0288f888b6c23955d1d0b4b5090c81528d412e3619e234310f172ae55240843cb0c307df0e31f2875b55fd96a028e1b869ed9b8c675ab938951777a9002dae8122e3517cd012d4b8d195cabe2a7b3aa8ecb2441d1624e42e86c142c60b3b04d0e76032817292513db9c74cdeecee259e887e5c48ad6d00ace644fec12e5bf0c9ec3412abe26f18c0e3eae17e25dfae60546eb9fd6ab7cd51d167635e1b20818f8434387b62b0409e0175c5a9f98a7a8cc738c88c90130c0e5440ddfceace968fca5d96328a97e1d4608dd5da6e5c6fa20391d83342eedcb7395f93c5a77d6faadfa105a39ca12d0955e6e9b5dd871641380f217f3926d86c0ddeb44f87f80dfcab5004eaf2d224db555d15d0190b4dc755bd6dbb4020951f94aba94d8d26ead3a22e166657b5c9e84b0fb0a844f27512c4ac79e48228dab47296c2ccddfee4f23f283043dc3f883cf4a4e2ba4546cbfa18a7b602f2f041da343cfbf0037390ca46f268d81bb62b8e5d49b79d6ccc69f8eb5c7aad8c2d2cc0080fc7371527d0fd92547fdf6d2bf73cdd591dd517a47bd48cc715c92196516cd6bfbe806a3d4c1165afb305cf2409f56649c8d09ec0c6be93298817884e5413a102c11dbec58fcad3df0777e893c4c9e03f7d412a8f99f92428df9de308cfba4374f3035a800087d6fe6ce4bc27f3c6494a036afb735d1bca637e4da2e668e34191967ffe89af61ee51ac70a33f743a1d4900e5d8a7e2d364fe28d8f5162621534ea5e80504b06594549b0ff66a52bf87ffbcabd5243c514339cb03e54f391b1c841dfc6cc74afe323fcfec33665f8595d84adc75212aba7d0b2f99e25f473a63a937e4395784577140377e819730f7dd1c9d50093fe704a77556ee801482de0d7ba9e674d8c55819c5d8f4dbcc0d875857c0e235a412761347f4f7fcef9e6435945582ab87002b72788cf286f681cf620bdbc7daeba57b1d36f8fbbe9c956601e1a08081752dd31062a8a95b1ad0242f1c9649043dcba76b4cbb7c0ffc909c80e06846f7156de5a4917440705995ac8b6bd2f3a92cf78259bd012832f79a6b675bfe00293c0c3a8dd644ab93f98c93027003a409497a9d8de4891afddb0288ed04547d5fb952dd36224191f3776cebdc1e1cdc41d91275c6f48d8e67b4caea659e4bed0a30314320da7f70dd8cd38481d571bc0a67d8399ec0101d1c822c849df42f78b050e98a5fa082e11d91c0e36ce342d19e95dbe7664924e0fc724f2a49b76699985ffcd9d11680e9c907d4a20f52c4f10db54a67d59a784657da9cbc397207fd97cb330d1a9f5c34c02707b2cf4c6fd1f78e9c5130e8f36462126fafb27ab2597ed95b3a8d31333d5f136e4c022a0b0e118d0dbdff325eed8505e7baab3a7e21003a9dfce10dc749ba9b07ab8a22e67b1d24e5702fca10ac948ce949e244b9ba44526b04dd3e935dc9d742cbe1816a933806dab6271043e2e950009f528ac8319cfad76781092a1f8cde7697bdc996a451cf669a8a6f2b432c141d7a6a3a116874e3d2528405c4ddbd93b154a077a56eedfdb9e1ea82bf7ddad79cf3a5b952bd7d585b83c3c8519eded3cf6bbc5b08595a39d23ecbdb409cde5355b8d233ddd3f253ba53a87b0725f4043a070822a4c12eeba2822f344d3834846c82562c7a8ae2c641d7d86c4576ddb2521963461120b15099b07a7fecb12eb76ff0d76ca2347b1ed4311fc754f345530347fceb68435327add78d9ff3919ddfcfd93d0092a78a0c45ba7c7c39bd95882b1c442515cbedc68fd04748be84798f9c0f4b57eb5bb4e8c3dafc1e91a9e8660f81844be4f70e80eb1ac494df2390125a2201873f8b2049e2d7a9ad94176f6c64a7e989bbe5bc0a85f0abfaf829884fd43fa1b8490107fb3e6e8fbd27ef37b36fb3b345264dd9af504e949311483c4951e03ec1a375826ea9c2493cbee7e49657c59bf7c29c6a9f60d7340bd7c1f113d92f6f0cd1eea0fee38c9d9e784657f8287d0eb5582304f0b77726a0371f5778374bdcd3a60ce095a8bb6006aee41e507cc48248a27e1a772f94216911180427e47be7eec750d6eab176ab0e0c42116831bec5319822786ee87273094d28bd52af098aba7ba5fe21208ec5debe1a515b95e8f622e4fdef5e916002d89b169a4388998464abdeeaaaa2a26ea746c1f4fff55db32547cbb93efff46790ad6e0d35291a405f48a4950790b32b34ed6dc6ae4561c8fafe0f677dced0e6c6a84e92de6011c1761e7e5d038471ec1fdcfbaea3d269f4aba783d0cdccba453e7605700fad3ce70f83442cde451217d00c206e7e2aeefeca654d48df2c9172d686e28098c5fd7982a675d2d867d431360cdfbf799b16770f502d69fb57cd2fbb9db52003c887cb203d9a158588cf5987d7b57441e6c16486444ee50d8d686180fd921913888fb1abccf97506283735bead6ce33ca9954c60cc27e8a3d3f46a7eb18edb75508940e3d99b6b2ebcb07ea80b943910a42c3405768a51644eff12191366eb7632f3725f7abd4e43b1d51cbfd1463e09d9ae35da461f4dd32d52e65e3bf0018d76e404a1f19f94c2bfa6a5c607a348201346b3442459413dba263017d564e08163a23e50a4813420edbd008049cc1ff18c8220bbf74823049094bcfb21f8fbb54037cb09c61bd41d70924bf2e8cbde70a8edc5c15e4c3c12ce7e6328b97a7ffc87602b58cf9e9088f74c7ed112fbc00a9d04303af8a62739ea899cde9f4181195975bb50167c9595f2d683cf2e6da25ee594bd9f95e920546729b28c90629ff5297939bdaf7aa9218b547295dae22a1518d30516d42315abfe2fca910df3959576ed2364a51f3d33926dc80fc90855b09885732a6734bae703aeb1071a782b4bd3b5e55efc032282a6ddefa6051ddfce66f391cbf559e0fd3b74d0f75e0a28a870ae8f3b65e98d1654cab0e833334c80fddec0e8faf41c5de7524d08202d31b00946a762d383846b67205ff1a68ab42e32d332255c4b2cf4c27f93ed536c1a02429f2ef36653c6ae410ccbbdbb2b94a9468e131c0af29d90c2ab4e7e1ec38aecd229c44525f2c5cad3b911c40c8534174294dcc2d85b20b43bb445a922f0d2cce0ae1c31b4537aebe392ecaaa6cae07ecb2f70c29cf878bb4b42f969bd9e0a241fa49f21048464e2f3c5a6d9f132789010df9aecf6f76564ee8e95861173c5d803543f6e200bd407fb87285e3169add0b51eb022d28c332e79bafa337ebfe1e5097f510d6374e8ba41c436c73e78897c1f5d09ec6b582b101ee7e215e8074bf57cbee161d0381567d2631a9f19e33ec2f783c3d6bbffbfff46e13c0014c9d0068d82ddbaf9e4a10c4d5b247103f745e91e7175aad06c8f7feb5bfb91db2709bea2b2c7ec99de07941b39d28be5bbbf7529b111f6a3644b1f4b2c975defdbdfcd19d90f72e83a3f66ccdaea9069a29e197c11ad7e24d91177aa4a552ed8c1c6c985a7b33609b5ab316d3c989e8074dda8cb973e0101dd3ba01efebc8550284cff9574d4e2acff853a52c17c94b02a7662b72e1b696ad279dbe82606d45ca3d93e9fabde4cb7c98da628835c1a943ae85c2caa6de534a3665f689cfaa456cb90838612f926cbc100d27c58886cb3e5d688bc4b6f7d1cb43ea89c6fe078497a5cfe2d90b693e297a3df2a48511e5b7d127ae2373389fde6b365f5a826d9e0f3abbabc7810af4c6be2c83de28104c3e63fb9e5dc14c409f52804671ead2d48395a338577e6e379cf57b2096019b56f65eff2e6c64653b8a7a9d736ee3845a2643000d5f39dfcb994d9a77ae0671aaf6eedd5b4fc855e886b643cbf3bce89256a42e02630c01f6ccc8bf05d72f665096e415962ce877a79d44a2db065e4403a4cb70c75541680479a10ee5197f980fa5402fd3baf9487bbfd3a20bf099b5f205733b6cd0ae8c38381db77c3008d70daa3b37437f95799537bf3134b6d68bbcb8a537104ee9df998d38e9997ba9ac56eece52c5c9ef64f5f43db490f4cffe1a68ea3361549268e36688334c579c8144fd47e836e01db5f20b7706336c8149f68078173f4e1d678ec5be9e10e55d3edc986f248758090120f6a51b6dfa319e3a858dcdf580dc27b1d1a6139d3d6bb4c611a796c6887afa9cc1d7a8c8bff7084a1a7370bfa2f6f24489aa37cdaa85697cb290f15f7920600b818b3ac7b8b6476bd037d182fee70dad4822f1cc34c1396b114c30b3b895dc062dd38371fb85716c10cb49176307967049581c0ea16b558c6bf139421498418e58552ae276564a2fbd937573c2b78bb314354ca40322d6442e07b9fba0de6870a3222f3bc272a648b75339ca18fbeab12618e61335b6642e1a46e00fd8e9f03a1bf876d1fcc17d9d64ee4f8c9905fc3df840c80777b07ede616fde49365c650309a828ad7214ca3381c050d518bc4aa4b88ce111bf7846745c6a502f0ce7d5d44b00a9e4f670fe3d8a42ae037572312ef8c9e8c4ed7b31a5a50947e9943bbb4d06c25b2bf11f7ce27cf70080d80a2c56a2c2aa2e9026b462bc595ed0a86d1599533cdcb74982d2b677c0ce7f12ddcaf578262029c0e5b8b7ca0c8299060d90cf4c693d79d8860faafc7551b6dc9e85b530313b7d4f54c3fc56b1a05c11cd40d0d06a22365919ead44aaaa29f75b89b7bb2019119b4ac36ec3bb28ac526558e5d019ba9c0cf9495749ad82fead88c4cc0afe710f12d92a0abe35bb01bc887afa73163eb73f80e209e3b7f8086466647033633f16847a0a7d8955080731930a6e1c6d668b7325f63b5eab1b52d2e31d2305926b22583897bc52c9a2bfcb1963cd0fe019ed5e4543acf65869ad9637c04ef58676d8826ba00e151cce3547446d320fed348e592de0bd8737cc8a1902bfdcdbc64b6cddb87ec3475614f6249f4f431797d4c411e19ddc63e1a72f9939986fb25b0437492328280a54ea12b6dfc80986970ddee0c45ec36a7c53daaece386a06e1acedc25186f8d8361472f3e4bc119d25031490915e97c5d2de9cdac52d31271e3687cfceddaae9fda719de973fa5f1f857aaa3fe69282caeb7d14390d7735a8ee092b963e22fe9033f0a52d55a98c332f79da64500be2cb631a34e9a2d3b143c24c10cdbeaa9a99df04f6565f1bc73a3ec0131802fbca9cb1d77247c5f9444c514d94e51a4d64f5bc70d699c0b538e74101854c5156fb8e6a7f773c4d9a4f9c2d3d05669c74c6462c92770fc14a75a074a707734df48472254305b1a7e006671491bc8c5e431e1b349767b9efc6d56dcbb6c242f30f83a17c1ae82b78805ad0060480fe8aac144c2477dab402f3dd6afc5170db78fdc77f8111839c7ad1f69a8e685ec1c5457fb5267d96d89f5d0c1731de96b0bf99bb28f4215770fe1e3c3362ef0a832d543bf3b835e09cf61024c040cc45c23cdf27d17f3066948ea621c76b6e477daa1c07847023f42eb9857b2008b841674ef61777fe4bea2304f2e08502d15549614abcc897a1ccbbbd701191e7dda7eba318884447f713f2ce49ddd60cdce94935c4ff524104821ca396883af54c07cc6bd5f248ec8056867566c054e968a45ff542fe369ef4cc92221c46f4edb0810e292e8113407db3442bd5fef3772cef00239a2df1637453ed0e24c16da467accbaf8bbf6f7a2f87c2538d1514994410affc0ad29b106745b2e91fc8bb231f12f13de050ee4262abf56cfa59c3491594966ccd4e52ed19c7b45fdf3ea7fa2ba97a5dec43135d2527dd13f501b68c920a4b049a2671fd148ded12266d400c93ca01dcd1de4420f68ac9d9a4fb5f35fd25920c6656d8773ac0f388df299e2ab6770e657aece999ed0e5c334930fae1d02a602d13e494ccb7628ea3f28533b4e3f729a5c5e19d097f9994de343a9d14e449873b09598ad634f01a4829b1af41e8cde4c011a7d5dce2e257ce2236a5f08755a38a192eb8d318effdbe536c653006a61f4aa3b2b4758a9caf6c4a9318370e04f8620b674de59f18e58bc6f9fe2356ef7b208df00551db315e082956643c96ec53accfede45b5ad9c0f18898ebbaccceb6e905cc7be2b29ec8ff41b13f97e76d4f77a072f1f70e3f07518d92ee8ee345e61d557b2763710d76e5fb4d786ab48b4c2f3b243b986e9a61a2a0678a0edad1ff9eb6f6de0f0ec85c46adbb519aac3eada3a0e8d7dc47774a05a1388d2c021b01f048cd5c557f379689449312e7dc6b82b03a839a3f12ecd180b116a2365115e5200658c817cbe36bf7735bc3d28d51cb0e2712c07b76893587469f0734824e9245edb389b25cfb85e358501fcd02ff386782484a067702a70b3e83a5b255bac69386f3373750aad5e193634393bc0c617bda4d281b32e208f95dd8b2202dfcceb3dafd2fdca648cad1dfd50367ed6634512a46c96d9b74c68e0d4f6f156a044202a116d5c1e2e05a5d76a2a84a905be5aed9b671bb2478b0abfe6a5785a6c54966c7b3ed8d44a8176be969bea19eefb8aea1bb87ca7e4e58f1175aba358d49a807bdb6ce2fa0dc64a43903ccc7917e23ecfb33cd04311eea7b94963b5bbc26dea94ffa0f915313f052ed1cde5e05f284d8e70e815b73630f1721d10e07b767516897d0dc8f19f97a6f315a5122496ef394b4577b8bb93ed4061c111acfbc9b56e134aea89d1cca46914995dfb921359b9d282c07e975f1218f4cc90ac4229961728221ba8bc24028ad1e5a18d3ce9c58f51a78d2e711993cff308328532706d3f0e368ec96393462438094541840ad644c0ebde7f09b1d5e8cae4cbf55a086d0424f85faf461a92312c97fdd2fc406b92856c46d09d4e3c59ffd2b5752d3089658aba8883a9970617b0517e635411c6ba0500ca917056d1fd63dd44104d2da78052903bdf6fe09bf2e624155360ecb5b5c01a9e3f813d92fba76cd712076537272313a8b38080ff2a29086ea9a571873ca4bfc937790d7c22753c1023c7eea688314fc5b4a5313a11d49d7ecc09752ff096fe4673215eab174cee6f824f10f6e508824e7e8788a8b954940ca97630817807962e7d9241f160e05ce8c9fde8ac27f6b30bea448100c9aabbbbc39b1e8462e4e4f4a062981b0d0f083325f42096a7ef0df383ac1c22815086c32c686388e5cae6694d9d1139eb761caea99fbf4fd0412124da9713dbaff7ab7e91ccf6c31975f72b317bfb4007896b44ffedb2912f43c2ec2008fdccd46e50d1c3609a10b235f706ec468e45fd097b98c2b09959a2468eaa5a254afeb883645fefc852706eef096810d5f2ef68f12d14d47e538ea61ba97a7c1f9d3918de99826dcb7108921b82d210ca276340cd48dc32c19004b73db7e7e45b6d912309fddf77d575a3de0207af6c4b9729cc3bddfd616a1bb14063230c2b2ae1bacae80a5adfc7d31c54442b8c39047e8c5f17d64e98ea80244f56fcfa3fbca23543307fa828f4b9b03fb074a2ede2500f48fa7a9c7749b80ad1356af59d275083b48fa5c6dec2da674ee1b7ed547de55146406a2a506ac1d95dfc46a81917fe07bd3410f1d807c43e5551355ae796ab1003804de72016c7023d52fd47a0741dce04bd8c70cb918c4900a31622e31ca70802bf41351ac7aea54656fb0daebd5073298ba149f47754dfcf7a00ef79707a670994ec3673747677b886874c122cb6814dfa51920be2084764afd418fa569d718ec9fb3f4687902a7b489e04a9f49e05b6b17a5ea6d9572f5405078ec9adbe29b1216d1c4ef5d274b92939d8797d7d6609004e5975757a377378e0268ff4f2d12876a37529f125b43cc9a60467643deaa916b6edb33fa2cd676f6f744893c92d4f8b414881df7ce33eb517b70aaffa39715595aa6e2517a23eb4a4a4f887ce41109207e87f4de7c508fdb8acfa38a390513d1356abca2d3b0541633cf1f98239f3c08b6e193d99b8a7ffb76d7c108d0349733a390b2c1f32e75a279c4b4b9b4f28cc4b8096f08a3032458a23e84c3b1d783a3c76180e32f13890eadca06b3d70955490aebeee854866e3a00185b79ee5ce09aa119bdf497eb70955e854c059ce93229bf75bca6e6a08aa1e99fd33c67cb68ea99c69951e8e75e55d41229ef675e7d3cbb50b4569516df488dbf80b81b7214eb3cd852a12de127fa3dbb77703bf597765b5d2259a7272ecfb84a445525e0034d24f1ceb6bd2448c475c647fd00891765719dc81200ca95ebd53a7cbc39ac71a80b0fc1bcb0166c7c794d28be7ed6487062bfd8e19643b384a42a39cf1a4939f222bb8506226aa2ef9dd2c40024c32c25403b7b43c5a6715298d6af87bbd031021dfd91d4a511592f8a9d42006cbf50ab02defd7d418e71d65b0ba67e34aef5d608fae9369be6b0711ab6c21c7b5f03a3c17f00b3620db0bb95579dba8ac0fe77acaab80fa0b608bd3c44d87f38e22b453f926604f6d02303ee7a0916e5d41d5f8e922152c2bc88c742bfe6a8c47b4297fbd8270388bba60b952c8d9b97ca5cc659a8d0b43ba42d764081579a9184184e9d645351db7ba283f61dcfb353d7f90264a2c13ebd9ac0a6df13e5b551686ba5559cdddfd531e66c9343ab350f5481b762cb8b967e760e3e41ff6c2803f7345a09b092674a25bd80fff0e579ff565ea74c241e3769b8c9f69a57050a4c79905b94b614fb46d1a789c016e3ac261b1b4d5db898995b190a7fd49dd07005a1199893f26b7dab373695fe13081fa520c8e367a309d5c0b484236e944bb0de15c4b7f99659f5fde4726a659aa17acb6e51de6f1f4974d11b01a76888eb038cd58caec375a44c9df5bd7832c2d9d4cdaa0a32001ebab58fe9590481284f6df359dbfa0b7f2f642bb22ea18174b425b7c86379cc4d3c72907be11d67993e15137fd69904fe3c026044cdb21c2b2aa84ee6427f66d887d60518892f6a053f7ea1aba96bd127defa941bf969d12da907acd7704a8a92d683a38e1a5b933fbc49858119cacef925efeefa297f3fb1f9bb3ec1c64712349402e21161dd63cae99b3bdd92a327c9ae3017ddf00c9f8a683af790421507649a886794f362636efb2eefb4029c4c308e25914079e2099dc88154ebff19ed0270a0fedec80bdadd6755d806ce335e07b4b91c3e8eb48a56a10f8b6b28150735e8e9e24a2660984aa3a84609f2b24f88b34adb84cecac71ff36c5cf3d060dfe571dea13126c775c7b1195fc68a34e5329c7e311c08757407e0795736ca7156fb964d218fee02dd42a6df8ff3399c99e822e755e2869a4a5385a29c5560656110d2bbf6fcd054c85833a38b2a3773ef19d4f12fb34ad51a64eb51f2b80775c6c17ae7331cb56c03dba0ecd57556a868633dd0fba0fb1ac8ff3b351e3861f506a1c1d07401fcea112f01e5d8ae9342938ccb0fa54d1421f2ef9960f3b159dbe225a53ff51756a52fc210aac28eec140f3e542dd71dc18b9f5f254b6a0d2e78724f072650093d565a37248ca49cabb1166fd78f79ef41e06dc06977cd793cd08b2523fa65cb8b003dce48da80cfede372f89e896f670eb9844460615f344760e7c0edd2548c3922ee0c6c8eb299553e2de25d01a970e5e491ad93b00ef9e7d61cd32983036e9cd631251af428256db0a5f984df9cb48b573fe8df4e5633a94e446d0961da36fdc47442ee6b6f7fe0c635116706faf613312206fa7b473b6b06c43f4872bc08ca93b2dfc7abb20f56013865705916e8942aea5d0dcbeabb24f9103de88b39409d341b5cbefbe04519bd9ba6c7fbccf49be45ab627ab9270784b19892d351a0110eb8b78bf37cb7f265a4321d9f19f44e110212b0ebeaa4d2822f3467e07bf7ce4995feaba97424f4f1db2858770868ee9e36ee8524ca65878144fb8cc00e72403d32bc5549d7ec45f17d33c37e1cadbf6410bab3442a7f7cdea4dd72afda490f28841a57e79e31bfda504907679672cd04b47b43eccffd3551b1738c52e7de581cb20998636354d9b6c80872f7d4b108dd7a23ba7e305e343c761739901f1d9d0e706da09783918add511e84cbc1048f2bea9f091beb4e4d40f64890bc91bfbc7f0ac066a7e337cd5de5ddbaf67fb231d21afb6da9961cdfae3dffac93b1f27e7ee5bcccc354f2c027a5bba204e842239e3a9297ecf8f3741c2a4ccd9a85611f4e8c6d0eea4dcd42e83b689c64ac5acddb6a398d52c00b643f78b8dd4b3cf9fde89d9fbf1324503bdf63c016535a776a0c7e7f1a417e79073335c86b3cc94944633ebb94fe04d1e5d802469a5ce100dbad00d57c211ca0aa6613e8f39353953c721c7ad221065d84dd90b5c6909a1f3bbfae1e8767e45cafcba423a95b79fcaa80a2d612c6d4eaab66b046931e7b935e423797c627a785ddf59d8da507c902509b8be6ac220120dd40031306c07a06bcfd5252f757e9b23eb2e0325080b9567bd0e258bddcfc5ede0e6210cc93b90fc24e4d236b0ebeeccaaefa9bb811c83ec691fef6125447a041d5af9161a00f5274d54906629f202e5ae00beaee06ea7b061e440e773621f899f3c8c1d525aa4b44f34527ce9178909e8a668545749c3fe3fcc8050df9c8010ee3377ea60988b1c1e9793b26dd106c237aee7697c712818409807adc962326f1fe0283a44a9e197f5040d1b949f588d88a3a2cda38678c97dcf8686d6ac763b088936802fe066c00d6cbfde761f3c5b05b97e272c6160bff8de1f9edacca58e62986c1126e309825df7d5abed33c32376bf3181bacae93a15c0c3833497b6b0f0533396d5017975deb67508471145c61fde124854e6dbdae73b900111c7e41772f7e6ca29c9418d49aeaa0d777eab68d633df064e87332513bf3cd8d8177da851c10ada418b48a3eb34d9913227e8c124242d50a8d68e403e7d09cd6b398eaa8cdaea9800a7c7362afe12369a5f4af50b65f2005736cc23a150db8b6c4fe3519edd9e45420cc60d8d54482fe2bec86fa9d7ff7177e5b1b45d07ba07f55275e72b957f15ad58238341e1612e0da005f89f1ffc280554a996d71650dee622b715c81027fc5d986544316b638437e5c959f2c9255cb83c5781f67ff61d194c1e45da037c7dc72ae98ee495fa60e85373de227b465867fe667aab412303ae6e7452a524c7124c577fe714d3099fb38b6be26f8a62a58d5226fb01624a14946d6213339eb42748557520b4430d83749a12d4baf86151127959d3e76999a21f51f74ed7621db205542e070a24906bb5a7b39d526ae9a533199ebfcd6ecc716d28a9046a6c17574b639902418d6096ed12e7fad2462eef95a8b777a5427351f0db4bd2d64eab85e42b76dc46e322120d495082c552dd3c77169280600e89b75cf534cb59dcacd07c47214160d4f8ff0191ccbd05ca8002e9831d80367d7efb04ad221d1567967cf63fce47093e54fcbfce1d253078da4754681dbd30cb2646a9e31dab82ab6b6df71d1577a67ac35a74b3cca1f2645578179b3e9a718f3a6c89b2c3410df940819a75f1e3571a7734cfc4d4d9bbd08295c65be74f4c55d1218092e05df3ffa3a9a3797821d6e6219756298d1a0c5ec34fd33d48e59c81af86857639856ab48eb39f229ad802294eab4c630ddd7a0540a7df2b8d2a86832b5ae53d6a5763f6c23b940d53a4cac3c6f61fd0c974a70489dc33b3f21a1eeca7aba80b0ad3c200c10fa914796656a3833e16dae4e1dc96cf6f48b1ef3a2c8c7ff47af727f069925cb919a3544644373d8f13dd5fdcbec89d851de1fd7e76785dbeddbb9482a5b3eeee40cadf54a1f9a66263495626ee414078199119ec178604de5923eed1a4d6f95dbfd17cb4b880e425676a1ff1c7cf21a949fcde78e3dc2aedd0255158710d4548269a8c4329245fb1c9fc9e9a9a4854633da465158cdf1cddd6b0d741ce9b39d7d683d4f5c3cbbfe5d18c249a9da29c42d691a128c60e88c74bcbe2f12ffab6ed8fb4a17bb08a7f369022e5c6bc45e7d5a6732accc3cb741faee5cd899410732f8d21d97ad021af222254ba7c191d5fdafda2dd8a1b337871a6ca21873b514bf71dda55880b8d6ceb11728a720db1e17c0d7330b87b313a66f3faf404fe6d564b42ac956d867facca9868cf94e3086ff0d7010a014e71ee850c3041e366c5dbcf398b277eb6a63c45aec1b42563f752af291879495f3b48590384802850bd202c4b8a2ddd11ff08dc682ebabdc39ad856e64b9e34c68895db940c7bdee360d516d325312ffda6bfdeae34ebd0c9adbec2c186093e0d11f740c8d05f41c73fa60ac3dd8d8c29d0b944bddf81e0b7a4fec364dfeaf4821c68308fe1aae34bd971b4a273e54376b4f59509273bad71a48d94233134f7e5b1b4d47d376a4bd138844e0982e0c877ee80eed9ba0393b7270d1fc629f0121c7b2536c97b9db52b324e08648a853a6f0baae65bfc58a0cf760161f5f9a1c2fd7204ce5dba028ef2cd17c02f97d14c6d950637dff8e2ffa5d5a58eff9d9e207de194c26d14f8c1b6a27204521e9042cce2a8eedc5fa5e48575fb1d4c4166d64bce04961ca7e588ec73f9c8770a74e22de236e498a7b30ee56987b4e5e6166b202aecd55917027ba0974af74c06c985eb20abacfd394693cea932873f0764862b6c542f83c09cbd91e5cb30a5621295957efaaaca8bcd7f98f7e784a4139e0338ca092a54e207f8b843b493b8453f18af14d82fb5ca7df672b6b9e29dff490f442b83b971d0f85c1547552a5963bd2f4f121eb979048013c9ef6f17339a5009743198eb0f6b42b35f31e55fc0a0ac2d9cfc456f3384a47b52c503b477bf934b95a01c0cabf27b3192ddddd1b3c4a6d679ee6dd005a615edd9c694234a235d4c9821d3b25abe0811422aff371be29e8b6a4d38e450a1c7321bc69879ee3d107d797aee6fbf9c52c3a5d9eba0091fa784ba2ce339e4dab826ebaf007668a80b3dedaf55760a8f48da5a4efdab20852b40e83ef7925a290e57cda8d5680f361b27b95b0173a4a917ef6031b9abb92fe5d96f62a5bd389a3c511130384b85dba8fcd5723d167b07b9b0d4ab432869ce41b2887beba693d5dd204e567132f261a126876b6aa1e3731a1e3da5ab69b5b7e370ac87eb112ea5b185f47d0751a76f5b539f3b1cfdf366b0a614afb945e4ca778ce593c9ea97e05ce0323769086b74fac2b1a6a05dfebf4b98754f3581a2b7e59c75390e9053ffc210d487de96039fba6f6341990a6c9efc3a1ef71b539ea2c9c64d5f370481bfbf589506d4ca5b069d17f422c1dfc379dce36170c800ee2140ad421609bea1bc8a26e9efac57fc4b0f30741ec7caa518ac53d205727c5db4dd43fa9069aee43c221556e86c4ced763c357299ec0705e92ec96eee13fe7d49d0ba763b3fa19c944c7c8e6461c9c90f3e68fc9c5d36ef3b29ecf7189db90ae332fc25c12889f8f181eb42a2a6b745e62a9f27180eeb9d4aac3e66868b3de06817ffaa4c9b10bd79d8de7315c78e81e21a19971f0cb21450c48ee19f9e9eab46cf519d26c54eee4a0217dcbb0fcdf25bf3acd3876420c4a12b9fb7b4189ef3e0551bbc9d8d313852a9a6dd08bf150d754c18c24a6aae2511db1a8e99e7e6c04476835d671c121917950a731de2ea8da180bbaeb13664f52612244119ffb4819862d410f553d57d1a03cfa1e0c7a90193b74136d1896ed8e2ce32b237af5bf9923bb9e80c444cc9ccc8d480f986b959943f82b10e4e988c546e862206770607c2250e1d6b6e7bd4022a47676b24117d8b06fd0a08b33c1daf6846dea66407b86a52216c02fe98a2e549b31890eb8d7259f06cae81d090c1e8eebc88a60f3592def26b33eef90485c9b9422adf4e173d805368360efc4d237931ae00deb356d6210d840cae66652625297ca88480019e1aea79d8ad33f652d64f4ae2a1abbf030372ab03cc41ab403f4d62e3cd233a24fcc801eeeb92ab08438bf150a0db8a61cdd57ff918c5529eb9c430fdfd00ea15b4079faeaa14eebeec4d2c1ce71b70b6529d4ce83e545b3b9cf8811f1854c1464c597ccff7251754a6fcf3ac41748fcb96ea8552a9d9ff2a6dcb5a7602c1be9529779203d2a56a48829c8f4681fb5748a19d2d83cd81e1a5d036bc83f20c410cf784963d7a4e7978f55950242e876b8c0fd3699b5b0c924716de41ed15c0435f8b2356700da38326d16cb15730c7351058ffda092fdcd9903a528325c5089f1ebdb7ee4551d2b8f91f560b53855c4d645abc0e735c60f5d29a1dd929cc744449e41be559954d5f1885f9858f36f1848cc990af45d3c15b9e3311b09858bc4a47155465701a1991b34cbaae7373fb485670684a9ec3f9f4d26f6a041ae9e14039ea748828eae240910f4aa748e004644df3166951918c56f9d4058180397fa19ec303bb4e17af98dddec41884d88e4bdfb739542fe3727e15a3ec6771d1a024f533d70b1e0220beb0fb7021987b6c5fea2b8c4435630e9df55357db0ca5ae82c1e9ce8885402fb7dd72a47248b5accd7fe77ce600e8857dc0382c6ba73fee8f2391d198d5fe5a44dbdda51186b4f944bcfa47d87aa369228bc33ff49c1c6b4fbfe67b2e46fc8674c4773ddca58ccbc54c93922bbdcbd307f961d8ae7196f178d22ff29a7199c20e3d9f10c3d678e000178461664750ea074179036551878da4c6f36f989c91f23e6b0e9d0b90667bd8dcf980808aac2d198e17af0663a6eded7b7fa028ee65d7ba2664e08fef7a15a4268b4bd6ff1eee0f539cf5592928113917515cc6a6c4a9e05aaa42adf63f1e38b88c84d9f9c40973d707de65ad4764833ae2dff0f77bc8d0fc343581f7da6c20bca594b7357e7040da0e6b5fea9283ea5bd0eca68fca49632b82fcca7083e616750c6564808f1d709e5a38f7ad77c0c8c10afbb56fd1626144a3d8c0dfb78a343afe564d463a76c0a634dbcab261e032486da964b5f94bebaf752be05d16ebe6dd61c70cf942ef9bf172eeec36c82477c79193b06d8458e01c8e5d66a2cc81d639b966516e7b3c684b5eaa6e17da7d96589226bec0b3245f0249dc6100a95685a033019347360c911138f98a98978d257a82205bc8c41ce8b5f26833addbfcd671f8661b573dd721e51265ef388e703e8d8816f2c7c3308bdf1c5b47b26c9d08d805341b680ea0563a1c18f9af11a5eeecdc323cd499c2ce82a69bd7340185d2a745d71a6a18145b9d681231be7c612b158e12f6c0e2651248483f5f6f43a3929148e32ce4dcec435466d575facd47d7e5f6f82fe216ef7ba5b920528e21e86e3c91d1e9cbbb608775fe7fc10d4e410a6293f2c325d1f6d778a529faef31e754f7c2f1f708f6c5e765321f2510ab4fc76170d5f4732457dc311ea144146ce3716cd1cc0111c2ae31f4713f9719a492d0a1114bdd63494f5acf0b7600aa619906021040d683604e88bd2bccd2f3949662523a68f9365799ad3ed53f8af0bc0a753979c49cb2c9343dd1d86d9ee5e55194290e08decdee613006aee9c3cbfcaf34adca80e948d02aaae7831ecc91ddb2aa984ce3188d174ae77ed3d23b2745af2147a5396fd56b57710fceef3bd923c224240b00414d3c5715989fd00c9957533b9c50735c9f4bbe6122a6c66a850c4c48e6ec37f7c987c360ed59091784cc5c6fcf3968859487adaa1038b9ead12130eba3fd3c9f5659423825ad95cedc0aeb7cfca63fb09013ba3a27d0cb4184553a8409cb368c0e3d0ad649345a3984411e1263b692ed2501214995e0463c37e13a517253c7f33492570c72c418a5c8c75f6f72a04c85c44b91331e8cc6a95557ea109b2a5ffe2677bd655c895ace2d1626e938ab41be14412f6020246172441c455c2cfd4b8222cdf3a6ba0c255112b34ffb4bb020f4fdca89e24814c9abb17bcf707b55cc3503042786f0985cc13fd9208b7c0c706298a7889a3a99e5d32a4eb5f61f04aa234d1f1d68ab5fa85de55d9dfd65f5351a705b78b9f0f581cc990d31bd7e728a3205ba5c86e375e8d8f3243f695e8c4ff90b11c6cd48ca8b4be5b43b03e4ba27c24cd5115c51adff07423d886ea627ecfbae33ecef0dfeabd687e3baa529ca8803db23da62ac74222eb13fd43ec23045cecd28ceff1114c88cdea2712c4227d3522b53e212fbc60e44f89dfd0738d998bf2f82b163d362a444659a4cccab100eb297e181c8370cea8e1d6fb0b1ba6df393df5aaaf7612679d0bc1b0aee9195c55f76257923fd80f12e165949d607f8de88a04d668c48afa7704e20a7fd1f2ade81e43adae1faca2d7087c8221e93985835d91c4f03c4104e54c45e684fa0ef4d0fd83c9bc82613fffee1daa5ba9aaed635ebc22be357a389ec3dc4735f005da8406c2372ff8b199356d14a74ad334ca0ff3a5f7aad27ac5f5bac496b114ea6ef825e1c942745d891567b45426567a7e9544e54b47cc3d531aa77fbede348ba57b88cb63261a005f83a88b839df74262d28b53cda6ccda9ff516383abe43efce1b8505daa7490f972ef36c385aa464052be03b18e9be1d1d4e86657dfb955920a8f9d5731c699eeba441e303f556a4b456658d7c4e76d53e8492f4c737f1443417356fc6bf3603b2e5bd235e751cd4ec784f90f1860afbdb13697f62835ef96180e965992b82007a1d167e5c7338d48c5470969edd9cc4b747a44e6c3294281adf52e3a7c97203fe96407305a9542c8c6e26e2af1d41a63494d3ce7cbddf8a92c2a06892ef75aaac573f7b1eca3326f06cca55fc364363fcef06896ee880a6f4634b54966979e5d0b6dabed5adb1f68d266fed110e5a7121002e816b1587723dc56803c8ba0d5490b68392fab1d4a68fc8c1afa200d2f2f0164479dddf12a65ab6ef37bc7a214cf2b8443df2b1685660c8f9ea87addbd190a6e8651973a89d3d3323d45620cdfbb290c24c0fda2410e0e73536d80ac4609693697de236a233805b4c8b3a050ec2cb6a166d8792d42bdcce96560b8aa89b64a8afa4686f232060074886766dd962cde7de56ec3a5f474060d178e48dec1b33b01ec16c50b23d9e7ad1aa3c3cf2aebfdbff0bc79d2b1315279fefea22e62fda44184f88c3f2a260a42301636ccdd50de13449c1aa5488608dfeecda0430b3d476b3cb44002941a79074ee9a393a4e1fcb502d82a6dcaed3012a988ce86070e25a802d10900c72f1a5d5e03ba50b6b9d4e5eea8985588372136517c2866f0d1b9d8374f5d171c190ad070c020394df1ac778511f5befa045c5bb34f74874225b6f4a8ff8daf2dd69defbc79e1852c8403efcd32262aa487c93a0b1515d05096073cdac2f3b57d9d72ddd9429ecacc3c66afc1cc645c4f7c3fa08edd5063968bee3dd43c16a06f85241e96b08bc3c51c4e1712f5cca2a3f68e55bf16e522783f4e1350573ecd56286cc09e81b0df82808e9b48dd6c0090fa267de197654d9b7a4a4a2fdb6540919fe9c6acf30dfb654bcd24d6a9da429c3f0222bc1874cdeabe226eadad596839ea1b9e7afc49e7f32a25ab379f70b2069b3acca72508a270aae64e316b985276a16da8c1cf6c1c163bfe8846f0a63d1156a351be60cbb01189991d8a8c5cd0424bebf15b22cc805b6098b6101e291b72594fac85203ca9e6a4f91d13ba864d00035123b13c5d84e5cb9f39caba9fe53c02307003987c1361b06bf937d385aabc05d07b2db7aaf47681cf68611db4ee53085d7cf9a52644b404568ac65f4955640e3bcd4a0ce21cf41860cb0efb1c3e1bc352c6676efa0516d13edcdcc3fa6d3b45592149e27f9f0c36ea30877b07969cd5f131d86afdf3b4dc5f187f27ba433a2a35f18db6ed8fb2403579e57e36da3518f9346ae142ff38f47fa68cebd38884b78e603bcd31bb91fe867332aa92e5bda4e3fd0c2682f227ac1a276fd653ac92b7ffac28b6bbb251d6fe51f090fe8b11c1ecfbfea86f7968c26e1ac9c86ca132966d976b538da8f4416f53493ad13df39999bdfa4a11782240d83ef5b47f50ee5023bc5526466caf2c9b05eccf22cf4b3e004192e2b1c32278a6b89b7cc0c5b51360a4008734fc3362cc4b1e7aa072d9f134b51d8fbfd8d49909fbd1787aa371bbc9e066bfc37868d8984c9e9347e7168056377b3712a9d84c252a11387555b4ae07d63ebc6a959143af66ba8d726e263516d627d40b5f5cb127a7e35a44e3972b0be3a2ae409a43f3a1208358e42edeb57d889306e8e53ef0547ee7eac9dcfe2f6bccc1b9c5278de74d1e43b7fc447272b49b5fae0757ad580905585eadb03f521f264c166e640adb7e07976de0b880050e3bc94ea995e0a1c1286f9d491a3cdd3be7d492f6d4efcae5a098ee5256ca8e11c84a195d83921e30256c6da3b8708c7a0de8238f128ea7f73a9517d44da0b0485d05537c1df4cba24d512a30d112a6e58a6ceb20d7d9aa2f547eb0f6be48de3a274a916c054e39af10dbbcb3e50a5b52f0e8ee5345ba728a28bd92d46698bc3f8e2a366ae7277489edc5cd9ccd8288174e1d31bf3ecd72498a710a95996bf603cdefdbc77fcd08528a81bc9e4b3d62faf048bc0f4174a7e24883d7a67c622c04b756cad63c594b9670c80fc7b73f9431e7ff5dbd839245c5d4466eb688a5e8afa804451bec95d1d198be568000e022bfd63d99fd0a77a17b039ea6126e6a4ccd778d24c1e5e03dfc91b7aa118bc244f8a3c6d343afe4570b42d7f75b6d6d804661e67ecb279a76b6f8ed440f552fbe56adaf9d02ecfefcfcb5bd9ed5323e09123adf112a240a1f1a728ebd39f929f5dfbb9f605c0d93e95d3726baca9fc150dde061400b71647432bd0440114e473dd8899c4d1056683b03702ed856522098e59cd932c05543ac04ebbdaa5a92b0c2ff49de4c358357ebcc1105ff8c4e540343602580e52c4019dbd87fb98a4d50dae0cffb4d7ebf4263e14a85e6032f6d790a0c17dc658aed3da3e7e35721214a2326fe3925e8d07381d59ca09883a48a11e12d65b42c07c62bd17b4473cdf6be645b45b59a6f9db70c202041b005647d1db69caf8729e717eeaf5aed25d6d52c8a8c7d8b5bbda35d80450f07bc243085fbe267eadb814ab9ccb6dbd121d0d0c738877670a37aab54f14bf48e7a4e5af348d0206a2a287106600af8edb3247461cac9a77b9770de3ccf7c056d0d367543add01a702b9876480485ad090251da0a482a43f2badf10a8b6fcb333cf8e3646c39ab8b1f080b60bed14e164fb283b7aed4c485f97d38ccfeb7a9890263147b636dfc1d63157d8feffde6443554cc0aa373681127ecd4d01f5ad31ce15553e03b93a7e8c2dd0d75cc8a8835c4fa87abe2afdfbccb1c9d418e53e5e12c08535492e8244deeda00404f9bfe04e6384cfa7307bf6d265b962524000c8bfb459feb4e2a74cb0a7edb45dd32f23d936474681d9b6a79a83492d717e6fd12ce202525a95e41f0ebbae2624c909a04531a4eac765e50a545b19696201731d42872bef45df07475daca244b1f8648f757e9eb1ed74aee067f0784dc9f76f57798fe76dbd0168d473927ca6ab32949bc54136971e8ff6f8c90a4ea5c5e630b852da07062376bbc08bcd93641f6376abcf76847eb490e9080987cfcfc0a51a2b8fb253d7d6b7628dac2d421c67de3a57b1492cfa8d366f61835df624cefc8911f7d0af23716ba9aca01e22ed0154a9aa8c1d9f2381e79546f0f52b4f366e37cb1a6771a5028500288b488fe46dfe02ecbb23d5ecadf86c0f41af8acd76dd8c5f4f82fa0c5f126e39c1ab1acdfafb2772d1e955fc7d7d0d0443195001a684b3baa0ff546bf8bc50a66ac52e236fdcbaff8f18717a61980148701d1210e9dec1ebd221ebeb85a50dc83a8d18346b24e9840f0ac0cc297f02386e4523b4792810379a3a3bc07dc9c31b8146dc43e7ee2f97298803e633618cb46b09b4b766a25d9147e4aa7a0ba73d0fb8cb5452c656b72152386f6b3648a01d548a6ec5eeca6e01940a0fc9cde93ff399a9d4972a0d7e5cffa412448e3104afb3c95dc8becd4f4012abbe7b4207f6cb109a0190d20b3e5ea98981c00f77cd45a175df8ec9c1f92467f69878dbdee706cadf60c89acc8b3808c61430934c0ce06fb51377c5744f4c4f93dd465c23ca03db9fbe9b491dbc0f14b23d7db602e74e22f37f8e27f44f00e4309624d3634a3e7183142ac75d32152193abf79ceb402cbeda6645b0733053613831727a9c92350dab27bc66e376c2b03854b603313538daf5e06506d740d60a9881efaba836e1298bbec8b7da27fd303fd80365317fa052b2d8583a1bb13b7b1592ec36d92ddcdb34d7674e9c8bd1aefcc5261c2759bebbce1ea2b056620a8e9852963818993da009591f32e05d8a15d5e152f159796b5c9158e0e04f22f5a12d15b3484de772829493807dc6f72f1cb9de5421c6739b15b7a8d4e9466a650da923f142e5ffb01e21081ed5051ec443438f179792fbab34fd4bfb65838a1be31510b09c5c828ad7e4db1ab54ffbc3634c8c12af870a09ae25d9dd0326ddf51daac112c13fa8f43d9cf392588b6716f16897246c62f92d09d02ee1508f0f686cd1a0d09870dbc028c9bea1f04f678ef73998e64a8f8eac2c08b9824ef8bfbbdef1ddf51fdacdaea4b0c80d236862fcf5c3acd974346cb1092cca823f0f854bfa26395e7cbcafc61a28c34ce31f0e51bb171e65d398cbe127508def14bf48992dd00863591ef886ecd1a3e3e2d60e762974dbc2c637333b86acf6c7ae6e9f104770ebeab6e876b3df7245c45d7838c35d5e1ef11530db53630d6261409ebd10915a77c0926efa64ac5844d9e7bc1e13820659bcf38f584cdfe6f3a4fb5972c8bb6166e122512ce516dfec16693ad04305c77e8e4e6823a74855a32bd7399bf35668514602354133c6e7e30138fcc1fa9d49e5c1e4bdab3cdc791f44ab6c3c375657e103cc3951bf5959ded0bb87995ca82465775a3f41e2f59f2fdd43ee9381f5ab683da19301b73d57d69326a5e63caeef3a366f4225db673eb2eb5d373056443d79f2ebb4cc7640d896e84b920f97a0d233e0587837cc2843904660a9af9d90ed67c6eca126441a19546ad8e38624f374e876a5b4672d47d924d5453e310c8e62e9679783fc01aba89e8d31d66196e0c06b91e5f04cefc96e201a914cc3ab8eae0832435ee4a3ba4566f7bf8efe92d19dc31d888c0d4f853d8a98209b24f5c93691dd8ee35ee8029a65054d4753f88f332e157edccb01553da6054d4ecc1e6f2380adc4b936ed9acc96e99d264b291fbda6de37bdcc4bd90010ef3ac043b5f50e6bcdcb7f27450453b57ee6f43b7e0f69d2c67b748af2ead6e86b2044f2d3caa8c8f4a8441f5378e879cf1372476e4c05486b2ee108fbca70ab2e7f80fd93926a2294ee75cc02f55544a50381fee9d03cfaa423c60b53dac9f7bfaba7616656a00bcaa8c84b8364c4b954e58616a634a2f9cf2992e9ebebfb4cdfba875b9a71af8514ea443d3ccb347b82ebde2ed0fadb46191476e075556cba96c82eb251a544a7be568bbf2ebfb954092705b73f9949388689b259d47a14341f31c29e672a0464731cc9203d6f7bf3c063de533164266492b15fa5ece74a7945f1425d30ad832f2ea01e4cef3a99e6536bc7cb754a3c097613c3d12730449651e6847878f5e879af807034797f3ac5d140c3fc18e7222876735ff8ea60d4188a8406b5c801c1f455e605784874e9344df13c118400fdafc0db33fad9c16a003e03cfc382ba1fc10c69edfec1145beb2984b27b8bde4cde110e5e5e006efe1c74b9ec8312a5b8d3a1450b9becf9079056187523bb37a7ca1be232bb3d48956cc67315509b8c194cf227a5be796ba36ea96c344ae1142e69302d02d08eed8bf8a6414e7c3d1aa4048f36e359871f284d2a38b7ba21fe9f3d290623f3acec04afde0891676dd593f4c3f93656536c4c14e743b8a509ac82ce50b57ad79edeffc9721207c4a46bfb4a90ebe95efebedf9d7ba4589e9aa89bc82844e0fc6e86bbf69224036411b1c4e9f9d8dd92049acc85348919ffa489facd09146a61bdf98b2ac16ebafed10856f5d75a710e82b4ae371d71be1217e2db2d0b07e3a08d21d8d427e5d5c2c39ca89383d7e36dbf049bac5547fea0f05ddddaa94704c538439426c3bc8b82d83073a60e934495634a4ee6cb62ee107de0b1798ecb5e0c717cdda40041b2e678299698c24f49a88331a3bd15dfdea0610e652a7b05d7ece61650dd775d6299c8f23ef86b2d8e823fb68f27329fc02bed21747b279d80a2d11dacc44f4711dc6acbcec1c9f99d261aa9d143ca4069d4dfc6f16d72fa25023b1c507b20b509e75da8f10b15f9b0af2c1198e24fdbdc3188acefb5d6529d8f08f5aca2b4f9b899c1f1df94634828e92855fbf628dac48a5234975afa598f95750f7c1bf53d13a79c3cd8be0451148eb7175756180bc79bc41b814d22611d1599089ba3b11b1cb743afb553b6ee62b928084ab6982b1df2a3029ed3cd1c81b9888403da53bd63907fc29b58f462bebc55be549ffcb2c9a491dca054926da891d77a2b3bad3674d5e25eefb14b7a4f776722260c96af9cdc815a761ceea51bc8dc51065b8f78c6f6f4b47684b98efa9a7f015800173167a165e40de788173986401a68d48b53b75addbe8b2f7a069dbe98a57c49698d7c96086b716f7aa41666a43c8b96b3e197ce49961f4d213f28af72bd9475b413fbe4ad3c1a5f1512f3babec217f00c9dd40f98a78f8b9558a338a50ae559feaf32869447a3e6a7a5f39af7f4ab836fb268c7a56a4eed6215e0b3d84a98003db4e8f9d0b87632234bef295c66811d882d037ac1b52947b1a8e2c719b8a67c029f6b2ae3637ede2d0a8951e080f9accc8ac343a0462fefc342de7705716207fb02e75771c8ff734e66975832a4421e887d7531e27e51f70df23231975aa05ef79914cba93be789fce8bb515c882632f4dd506077ee75846a5453b6b4ca3b693f30f10f35d4a3d3e4bf1844dfce8456ad93e08a59b5e577b9c46d1a72bad6a844ec24536ef80c85af3d3a1f8243b4388b8b7201540099b09f68f63127664e5c927b2faa662bed300e1f3da35090a6f97bdbaed673256f409ed1c81ab627366bf5ea8aac7f544a96e1aeacc30b930b442e74f677a7a52fc3717448ba1da4188569a8657ec498b7ffb48599e341bb5b47f672030c0b7476f07f2389928e440f91f9c37315bf6536a27f002c26abe5e2d800d11ae8c2c991f00fe8e00b949c44ec43d94e7f0621565b4b1277a63d261e84013798567ce5a58b76d816795295f173bac8584737d93f59ba4cbf4e9dc8081a8b5410216d4184621e09a5cfa458d205c8142a8bba4ddc2b49305e95f46b6e4956c7d8412e8d6438193bcec3eb20a82f69ffc13e2c3101c17f8c298428b9bb961e9a7855f28f994e4eab5fe265b58af1dfb41d56757b5ca43a1344e6773823ac0f6ffed9c89af53f670c5f21c160697a4519eb6e2b32047b3c833c2224636faa6f743d6ddad97ac42e07b4d10c1a30d08ebd5bd846bf632ea40ce82e507186cbb87d150ba3bbd27faf1e1f23d8b53e7eb0e06ae08eec9189cecc97f7c3c5f7faf7a3f90bc468fe59ae0f1aa8fdd1d75ddc7b121d17532153f44be88f97bcbf4d6b264f83935cadc4bb7468e52070a10f611dd1ee2dc018d6ff633bfa7c6a8d47674c9517b9ee83624bf553a35235932ddd23112babd5d34f6f324ea1bdf9ee5805c3eebb17009a3f5b5c453c1e6ac6ca52e8c917f4c2891f7a4347f410107e7f9f9b7361157b53c09876a8119b0d81ee77462fc57a5def151d977cfff556f71489721ad576b42a757fb86e1e6550371dbd38a3b8e566ffd9998d79ace31bb5859ae1c195e81cc1de953e90995cb024d3bbb381e14f37cbfff1371765fd84c56f42265f96854b4a2aaf61a7bc41c250e0c2e62ec1fefb3c32b58f6f56908abef62ab34bbed9c99c72142f5ad8d51d43be47f2c1d5c2ecfd2651637d93ab037007f594bdf0b1ee58786fb6e95c572a949aadf4486e53f54e9bfb44d550d8cf9e3b68017c5131f8c2950eb496fa2969bd0058a05b3d8444426dcfa5b1cc6802dc93aee274dbb89f8d45502b257daebf5fae10fc28be67ad50bf05f66db21d2e75cb0c9a3548077602030db4ef5a78eb8762876511226ae1c82b50a7abbf6b44b22509897828b8342d949275f18ef08d815e005b5cba9b56fc74cd4da1dea45a301c93b7a42af53c3347a076011d3a3429bce39123875751e2a69dc82e8b2ebc9f5504ea6082742104371f7ea4f9c66c0ec2c098ffea0801de68801cdefa12d9c9aa267923c7bf999a9f8bc329ff697032ed60f6c1cf55c59286cb8628d386cc238a1c13b8dda751eb18865da1acf55500f25240e22e15f47b56356add8b23e06d66ee031b5df49a1bf93ca776c55e00a3ab603dd4943d69911ff71a0172b6b4b5b077ec061f4a187a6a3781ff19728789a050827a6e492b95a9ac1e859ef32da4f10d9a936fa8cd9a81ae953b3cc2ed40f227c5a5496dbfd726fd32153d6304ec03dbf910880cdd66de2b05c9ed0cef696f6e89bb4b65b7ca577ffdc817272ff97106c0c1d612f0d4a1d161d47423126cdb20178927ad8570af34861ea407be118a716c71243603233ae9ec533c9ef5f530b7212a6f3234a9f9b0939e60e92bbf0559880f342cedfbb51ef316b41ad44840771f7c2da85e076ec6e5695b93089c313f9a5283deaf24609977e7cdc5f6f108872becd6f8f4d88599ebd308933032630ccf3ab493dd19ae76b3db12d92f4e67abcc6521b2d5da7ca004761c623eb4e02980c4196e99947aaea09831588bb408649400ba49cc704871a965966ec68d99121266ef514e772f39f09ce6e5a913d0fa7740af3c1c8b16f628f1c2b3ce504a8495a4b27238ed830eaade6bfa505948f118cb3fb30a5df1855e6385361c349f4d421671e983d9407ec1d3f1e31d6fd3ebf895a7fd7b348dfb18ea40298e83fcd49bf3a023f54d28beadbfb95ea6f7b506094905fc9636b550ea0f37c43d404c22446b25cb7514815ea9f745a24d8c5e9cf0702500d97f669a1acd8fadebee0bd730adbc9e6f0029315bebb0d5c857edc6921fd73775373b7c9572d99a8cf48e87a2cf3a8983fefae7ee62a2585d2f611965560a539b563f36d56123c226969f11130d25b763f7999a37b2f0eda33c64978ed661407fa474631b424607835200c87401e94c7883c919f744d8fbd67afc0d7d0662ca23355b2574d216c0eae7947763b3dc3a70d9efccb2d54914c9f2bde855d0fd46f6c6295914f1506deb5db5e7097fdcacc049f33339d6e366e824513e1c59c0c6e871ed01b2b943e4a14c5440860da31cc3d20a67c7ac0ff6209b6db3d7eb4520d14895b528ad2cd863505f9c0a971af0e9eec533bb0be2be393029c10738b18471264ac8d7af4a6defb4785b55464f1ad517938dd6e37317567431b2f36db0906dd35003cf73c075b91f0638a2521a9fdcd60aff08bb1de4ed845f6e3346d428a34f3f18501b875e659ee6c442db4acf968feaa3627be204066a62c7e62743e74612da68b0f80b4f141af7e691ad322cf6247ca76407b8484a24a9481aa7454e4cf043ff864d91a9334cbcc9c368de6e89364022959c8e3643aade3e0fec0f17fa701a2f1feca24f2f04e8abed4ed5b4dd6994b4617cea0d14ee8cc786fdd9caa52dc994cd91ed19ca97b092f1652741e487373632ee1c79b3d5f437c9cec92362324b8ed77a64ce28c2a656d86ca9867360fed99f857ac27f0cc003e4997c4314c2e57f3d74fed1c0bf8ced948a1f643809e0129ae4ecfe1ec9e553af4903e70b0595414bc2fa18d1f8dc17f4d6442a1fc68ca67b94da8ff08da569ad1524f7acfe8ccbd5b41fbb48a65d29e7b56db66e2307fe83e48585a4c749c55e7417c04b426d1b91c1b4aaa0711ca4c2acad1c89394e78bb38364b1ed1a924e5820f6fb864a01ab24dd05896973c82dcb7b6da37bd03bb378755f5893937d6ce72f8859d80be7fdfc92e3306aa04593151685c4fb282e8b1786cf30db186f0f6cc9385a52317b88ea166b3c700b9e6a3894dc0287e5d58292bf060dce40837bd8622ccab316624516993caec1241833bd787d4005ba0e37ca31e068747a4552931c69285ce6d08acc891c49682ee06e8bd73f99ddb67c4980ff37c2bd2233253b0aab34cb85cd4eb999438af9d0eea02318033a3f689ea7fefaf4aa94b4102f19c4372cd7d0f596f2a0a68f13101dcadc24145203b15150c411204a5f182a33e8e5c39a455fc24dbb70469eadadd81075c832f2abd28ceac3981d4c9d040713c71688527d8e6d8c061582a263058f2270253b27d63bc402451cd4c73dc0c50fced85bf3cc1214a6576d0d6f91bce2c6f99d33559fbc310bce949a2c5dda97d41b891341f6762f303fac72b0473b9a41c511033a5b749a978ae760d86a7b344d03b282dd87e577ce46a118834f2b6039c3b52d84a1906c53d15f5890b56690032b2ac75257078d7132b765c6898486682c1c4be830e6cc3e6b2073200c036d6be0bee12ffbdcd9e4924500adfd6624ce2ba0f38145003c621847e43a7f60075cc71f2d3a6b0c2c60ffa8616b0b33f39efea26ce96b5dd9f4a38fe4310cf35c16159f4f3cfda737512c5d95822e0be41f99aa82c8e35589116cccfa7a874e79bd84f6168d2080032a77953d14282b7971a298fde58968d7291104958e2caabdc1aad2a95a90dc3bb8b059a55f4a0688ddddc250c2bb23e7028cf27925c0cdca4ce8e7e856f759f5f59eeea8b730785dcfbcc722236ef71e71997c6e30b381182c3d7b93cbb8e8c6985011699e31631fe353d32ffbec6f2e1ca7cbbef177c32ba48f7891fc1e5b78733179796ea3c2fd54cc7d84234e39866c91eca7f6e417ebb776203b10cd851d6e4299719efd2b9d60224020dcc0c4f12567c9b963f79534d16e41d28cc43968afd0b673095a1953816641804186cd48defa21434cc181ed619f46c7e07b3b667d49da9c5504e066eaa2b3127533448565cdf45ce248245dc636807c520a4a13ac69679eaf77089cb88859c786836b88914c718c62890f16a47734b04e6cb88a9f65a9b975f4c9ce1f4424b51bdcd13f1fa3906d5bd2e623f46c57146dee5541c2dc0ee561ea1dbbc2cab2a8e6f16530952490d065f732ed6a2cd39adb658c0fbcc538f20966d770b367d68d90f11802e759fdb9910171b16095460af5595611d0b0be6d0bd8121b65633ed0d9c08971cb4e8d625c7137ceb953c319aa5e2f9d20934f852e1c3e1654001904493040bca60d35a18601821e9978dbe5e7da2707613696e496e3c0cdc364db4b50d19407ada17175ae1fe2fb26e9eef6d756fe980b3ecf00fd4556072713413e05bd0d4d2d90675d993c101a1a7eda558f6e8c080790151667ad23d9ee1b113e4e5ab346d64033d7eb37b1517792b9e89de74db6d3592382dc02597339c8111d53bc939a8ee4540346eab57cb02b04051001edd413e45c21b211685c049d12139b6f263d755d5cf05b3377718bf115078835c8d5bbbfdbdcec15cd3a5438c01642271fe6036bb7e4d93b02a7c10c1b2cc5dc4ad22e61b3296723d9720e9c92dc07e57f99c95ffa62414aa022a0672d8735a894513487813b9cc632fdac9d0fda39bde5eefc99bb2138cb48e0dd49b5c8ee76c1082b74bb2f61a635d772883279b697447fb7fca3a8f814e9b08c31249df976993cf00fa6a7946fa91fc31daaca6858768a870577d195a06a5879121dd9ca0e163443e9fd62b552010405e6c80570b531faf59966f0a5349bf9f8808e70596a3e8c5b98456e0ca6aa4c48843bc9ae5caff81d94c1819e4ae4f8c2f7e5de78d067aa20cbb208fd8ba276598f14e00220b2219d21d193b4e4e46fa91e1e282328569fae383d74e897e2aef8e71f4611ce3f52294cc3ec9f0d275b4756148c9a3e3de6a9224f0f27415d85539974dd0f96a457f202b97f800a433956a7c146c6fbd76af6fdf4bd151c1471cdadc354b1ce3692ef977b8af64841560de33d8ffbaf78aa389a7654d524389481cc00e886eca520c9764e662aea74f2135bacb58d5208c11c6caf206fabddb574ddc8b8acef3b37ae5ed74c037feea5ea2df21cb45b50554fd13835da64a68b7bbd9799947c1795566b642264f311ab1da9f6b4cf657043cc8bde6018a05ec76b3559dd68360cae6ecc6acbb29bdd320d33e310fd94101421aaaa0d36e242adf62e183ea7fbb14d89dcbd2190e59b6b1f4fd2d881fc650d51f5ede969897ae631a0ac88b25b30b2ca77c4eab966cf803074ff00f3261ec80f7b9381f6f1a76e568c92347667456600a5db88f41b329aa256461f5b6878abab3ed700d17325ed4eed1ff2078c231be660097b7f5b436fe3c64376a7f2c5d28a230ac1c336895ed11b8155990aa8ea221b00088aa8f9dde9def6a37822a64a9fc447dc2197f1b637abc505a34e68e30e8b49b875a14808598d1ad330e7919f273b375b31830214738f264fcbfe57987a0387c2ae64e9c61a2cd01cd1cf29103dde3f13881fdf746caaf062b8528edc1115a683af3f328dffd5addc20e97b132603042d25a53a39032b9828ada2b94e81600d99e74d7006f411bdddf03d08a52c5458c359129f66b39a7e40c03f5b6bbece1cf1b96f5e37414c769592785c097b59eebb4c6a5bcf761dc2e61afd5c7ec6debcda9aa0f6d3f3733c62113adf1aea7fd9f517880ac185a79e537ea21fd9fbf6cf382e552fc0d64c05e2bf5a918036b76ce3c7a348a00a6a9e60dccabae5bb492f8744849532e66b488179388c249243484a4bbb21ff3c20a52bf208ff6aac898a40075b04390e4bc71433b374dbbd35143ae4ad37ac55e7aec59581858c8bab3160cf4db92fe5223fb7a662eb838e8bf8788a103e2bffee57dc8c19f6c7b2d39f9b76f95ef6fc5b48286409d33b952140d5107ed3b1c295e0660cee237bf7cfdb02392efec6cddf23c4359ac57bf0e2f213ef4e800fcc1a5a2da751c289d4981244c5d883ae8d0a0c2a738f48adeab89e1972202623ac4adf335174327da64ba9ef5d1d65f7ef8389c15ed7446948f4bd837ea2d394b27695aed87a0138c25bc7542077930cee81a244d7195ec9d6253321eb4b0145b4208f339e432cce4dbbada8aa91ed7492a6ac3ca548989ea015fe6fff422bbdc7307f89e6faceda6d1716b1c798b13fac418ad849dcb7690cc0458810171edbd50b976bc12e2c91b123de0eda45f797be495426484880212dbd48a3dec8bb42f23f5de679d1edd9dbcb0e89402a3bb7513b2389f82f936512bc863da34799d40e0217fa79624bc25d5642649da137273e7555321f336ae22eb0dcd4395f9712f4420758d791d13f868267cfbf6c9b52b41fabb18decdd26e8f79e4ece168c3dfa09018dad8ed6b4877a86e2f81a3f7b377b13050c4de15c75549c9109ac905ba5b6c529dae23ff87920726181a55ab721b3f44ab40089e128e18e66abd3958c6db8181fa01c77d17984893ad7ad4748d9ebb9020f7a728c2a12a7a7b3eca9dc5cf1f5bef9222e63324dac730a615516901d1229c33fd616506bc428cd668c6795413d687c505240ad47aa83a3eacd9a926f7d6d218220543e68a62e058bee20a5a388e9c9465546a341d7fd218479186d27b7da7a7fa9b13e6930d5a41c19c2eccfca20dc4939a06d02ca869e265e557557b86b411e393b15a8154e1d391535c3048e270c31e43ac66b2bd2a6aae1417d4676032cd31db75ea93f8ad559371744f6359a57d5597f06c3e18ff10ec4d62a0c78c360a92dea1430262663c22908bc3b7d7007a0a46984a59c186f0900757e7e1cee6722a41cbd97a4112cc40441882e6aa193c481b02ca6aea2f56ddbb60ee08f57d9f39a17b1e81d78b653aaf72ab3c466a3e9800583d750b1a2c0c56030b14fa2a9de5261c2a54b0daaa042ec64f6acae3ad22df9f526d20b0fae818c21e1aa2feb853778f031070f72ae92e0cb9718b9e1331646f02f17f91a8683a1d9ba06ac0b1c485ca4e799986a18c8149489f48cc715b44d68b2663ed3b3769616b8fe641edda11e5291ab064a416e85424e6a95314433806fe2830910de1310556de9f8bf5e482b9401a0587276e6a001666cb8905087a3014fe5fdf5827e25159ea717b73c5df354e9b32c5549ecea98fc76353cdab183553bdf2bdd651d566c65cde1840badad284d8e912bf096400711c32639b3a7f5721e0e4befde806ba4a4a1c8f3e01792eccb54443d735cc9149e0a044382b041a3102d00cbf7c1ecfa4e78d192a1c6778c494a37f3f6062149236755387a97eba47ff35a1b2605a9b5e196266a45c94c375ec4a3bf3eaf4454ae2addcd4ee991edc240434a44435ccb75540b4e7121625e45217f28b6f049223db6921308567d85d2c5829030d6c6f93575e80e8aa10aade387191d5bd71088acf5b549a25f25ec6b8b2386c20f1a72db503113037bf16623ad0755f380c8f206405573dbe25b50c5b247f2aeae87ac9988c0c27f04deae649b3e03404817b2d23de2e98fd21f87439d133162a0f07bbf95bc28a3d89be463fb1b3cf7737020b9a417c8c1d29eace1d6cbbab12a040b3e87af903260787df1069a8eef0e15582b3a5324bea0aa23b9b3f81ebf17a4c85495766c68f8faa852cbce250751b7e1bdc10a135bbef1117e99d4cfaa88ccb201a7da2ffa0f958eed3294574758d0c6f37f9c028f543a055b76480b5ef089f43e427c9194be389fddc7d7929849e5ebb9dc034f7e5395a52f3ddf9ec8cd2c2bfdd565e4b6443d6f6c579c9dedcafdef38051d592dc8d4070c89b1a563aedcb7438ef0bf35070219bdc7b571b147acd8af6c7b7686eb114575e6c3780fe3ba6c60ab41ada8ed414198a3f8cba30da16ffd8c7592fa25df5c8170c16175d74f34595793fb6a457ec77b46de5bee5d2f9c490bdb61e1b62a26b415a80623be4599638c3578b5fcc2bcd58b2c12967c0f0cb86d43896f47d0014f127691d747abba7dfa6c5c1f979c1df121b44f7eac642771db0dee023d881648729222d485837063c096a5c350e3685db34d1a10ed0be0b44083aa29a89cdf1253aa5c35bb3cfa524fa4d1a0ddee603f07695a1c1ea9d1a36f571afd7e9dc86651fb9265cd5caa8c07bdb105da1d0b63e4af61c6ab878cbfc5e38bbe277bebe246a9db6b4a66735bea26a4b2dd6d6e4d86c07e4806cdd9efb8779ded83f3c4b9db13a45fec3d43062706659ec85a311539c92a385c3612b12232bfef62aec461132abb4a5a9e4b2d14ca0355aa613e6a0af361b4cfa5bc06af7d6814c71df7eb02405c396b70db5330bc9a3787099712ce1718d701f00aeaf0421fc843bb2a9b61f817feffa4d028a3409e34a3936df760b97a9c7bb63680a3ed28d0176a2a465f47657cd989d1f1a1db40b6cea9ee9fe73a7868f545be173f501c5d25033fc82d68e8e95dc68738a52a1b25ff9610ffd10a54606337dc3594141b65e6559f353b9f21368d83070861a17bd4a21fd2f12f60fd163d43a3565470ea198f1a343900a57342566755811d03490918c6c9f1e59a14cfa56e4175efe6de6d55139e41d28a6e1fc349476292b4327b4f18bcb7c4705de86cb33a162648f566d06d7c99ca7c75205b8b906f2c7dd6535e50f8e9a8c20160948c38881279d7eab8d4fdb5fdc6b9e351e2abb38a30e981bcc38c2a3b07c2a4548d3b225338da653d4a6b68ef5b0df803ee108b348fd3ad06d6b99ab8cb5c5bc0ab22c567c3e830a4e48f280627481a8c017a89e86e7e227bb25080abe806e2d8e64b64c27352b50216569eedcae115153d3cb166c897fa4a0c6786a64b9ad6f3c2a708cdbf453f9b3843fd2f3b2db4ddf78142d624b9a0324c52f1c11f4ea19baaf7913446778d8b06448684b0c27e3d2c7a4a37e39cb8bd51fb6e995665e201a32b25e537e8efe125d11bc2c3d8eeb577fc16019f9ae04646ec109fcf802c7ba78753103dde09a37f2e294b5c00999ec2fb73f800a2a0b4f9a602bd87875254368e38db143236c1d9fe4c4bd8f2460d78d26878c430498ec398c7ee728e43f91ff10561b3c8d4f71be1a9e19b282d1d4c17cf070a2dba6cdbf059bc3cce22f183e06c45e8ff416329466f5eaa736ca12d6898f7ca6ea51ea3ea31330d978e5837668eb54b0f729de1c80e415cbf8fab7f63690574213e573fb0c687090ba83c78b4f326adfa83d52122e72e01f69c6ada48ba7c6375ddb1d9c856353427befe1b3c2fe782e418444877abb3868e6692ec46ebcce774f5361cc5ddfc960fef5a6a649233393c1068f307480c646bde08dda8fb097df45b6266ed7c4cf574c62874fc28d1801d67ee7c88691dbb0db13aa8de924cd68ad967c6fc029627bf47d6ffea5384ae8162c677061fd05825d98cda41732f7191b473472ca9729a6db198e44cfc4d2df03c45b02165d847814402ac2ae6c7bc6113e2ff5b39da9622637facef3bcbea2c7fd8413ea6a75d80758fee88b414cedc1dac56c35eb32dd8beea797a4679744853f38571519be6bf37c8d4cc3f9eaf8878a7b07c16b4da5f3bb50d6e23bc63210a0ab6781db42b8864bbe2bd44d62a780d7f1dac4f9473304939e70af0215ad0349f472ef810580f7e8f034323fdafd76ecd00f3dafae4fcdcac0ddedb58af45f0b5460d3dbb4ff6520c9643398258f8cfefc61ed2928f6b6b4efad0a5236ebef0d9b44f228811c03d5f0f165ba7abc22d7a2b0b34e47fd7df277a5e943586b0a3cdd5dc3bc113796588bec2cb12c4685c98f37fb7a6ed86f401affdc9d8d47b62e61b82c5a6d818ecace230a0c6263fc74965700eab1f4d784f600f906753890cec6502134de7f33362e14c35e596c786509d765a03fda44b55bc4ee405b15ebbac85f33e29770e80bcfe8a17ca768725ee160585c9302eaff118f13919e52b8299af175b1065bcc1ab21b45e567ac69f5058ecd1f863f55d2e28642a33caf6c2c19281f1e343a95e4d2138e9e0c1388a4dd14d1f03a19f41a961a1a3cc63c4d5e658f91c4877f7dbe56160732d785d0623d7d9052d8804a3c788053fbbda761977daeed730912f5ff361982e9e3a6226ac9657f622c1e3c7d7cf6000df91aea1a60be27691bb9897f884c0a4359dd3fc38322bf9fea6fc0dfae2479dc7259451e234c4e0e3b401a77dd4c60e1547841ee5f51d70c9e9605a0d65b6c19be0d6652d6925fc4a7ec8b724834ef3d1cec1eadb7479a1927e475bfd6fbb85d92d2d38e1656beb7a5e3724568ee3802746a9714d18c325f284753ba0f24fb83fad61bb6dfe9936456a06226cac67f9f5abdd62a29110b90a66f3bbdc86e5f383c1346a431c9e793ec34e0ce8e645ce8d27dc89877843616033dd19d5867e5010a3b45d1b8edd8fc800a0cb6686406c1d6b57da54428440620d0b47feae25887b44e15a30e78b9f774fdbe87cfb0b5fe78872e4902ebbb57f330d975d397f8cbfc02d4a9f696e83897cf0666957a0faa613d497468af42c5ccb684fe81160ad162eafbdcc5f3a00cea8c40008ceca8acf936b812e481befa52c082f27b8e2c90e94558d5dd9e6c08823c69a648d6f38d95e05f587461b415cc7e04e5251b41b98080091d21ce81b7e82d732ec705353d8869398b74fbe0e352812b6008e874b2e6bee9af1ad71f08b76f05b0022a93664954e717ebd6785005b6687b01e7624724b9dd13b11bbd383d6446a173a7bc90d74dd1dfe59c24dd8d00627f9534ffb52bb49d1d0c46bdc677884b892d822e413f2465486fb2d7e9f1ab7c7e1c74c409134fe9a3ed2175711d7f85867fb490043e47b0ce1f0aa9e38235995690ebcebd4ac886e18fe3ab0fcce8b492ff55a67bab241e46f1fbed38d67c627a2de4c5cf914461962a56911f4273538f4d4ee1ca50cfc2f29da6a7e6f462ce53003e4e02633acedd60fab0e9877703b5146888674e2b73897bdf09a1ec62df92e80a23f7815848169997e874acb977afdc707b9d0b76e22d6c871d82395b081c82f3d259c9d5ffe49648999aa53639ffcdf558b30d0b0969d65625bdd5d42ac2ce44413d4ae264ce7ed579ce9cea7ac868b9550e95a5ca735e5d7c901430b3724bbd99b2430c9315a0c1ce6f0883de73d153c85a9b8f62bbbee8c90c8b8fa534d3d4377c2b670db8fa1b78446caa9ca395194b500866c485fc213b6ae1e724fb10bd635c47f0fb9b0a6d103cde6d57fb30f7dda6639ab5560d562fafc68d9bb8c0e82e3b55b466c01a635bfa62c25cbf60681af03f9034d5fbb1831f72b752572934d6127929b41c625c8eab0700853fcb1a23226f7beae6d631ab2b580af3f7fe80370ef921cb74e06a830a91b297cc27d2060b5eeb9fe45388a47348317f203a3f850f3c65a1e9bf6b49b164d60ca1600bc00b1fe91dc508fb097e24c6bd64f7a53508d5379ffe2cbf6d450d2fe8bb73a5aa8afc0eea19751d96cdd261a9d026d758b105a19096d71f41cbabda10076ffe0c65b7907e854d58e4aaf811a3a36957fc6b56480fa0565d47953078273877c349b467d6e43b04fb6f72d2a8aeaf9840bf2fcadff91ce37c3eab82b8b37c90b08aae62487ece3a6760364cbc0eca417a583fc126f5682a4101749925bdb31e9170411d182478af47242ba0d93ded830d8b5e9b5f8128508783eb7c43a3a6d2f20f361d3d3c14b166fcdb745aa84962dd555bb87553114694af9ba40ff515c86ed50be88e6d9828911a0b70967a3a3bb7e54924a4bc1a5820bf83bf25b8e657a23cb851e667f914be696ec5396e557fd9292890db72b0d52e61c58ebfeb5d368a0ee31c20dc5204291e640cfa3cf9af7f3a77691275a5a23b30a7834f7056e95801476f3c40c04935debf8a1a59ea872b15922f237200772996766459c7a7e811bdaa574a6e64a73b937e5f9ce53ac5c2cf830e6a3a4310fd78aeefd47f588bd7ba737b3d055c505fbdef46bdab9314b62a570e2397c7908256a14098fe49765577f5cf16a3ad873e2875db3c82ec52d01bcb1ff140691688520a2001aac3c3a2f3022a88ffd478d9cb9395932244d50b60382119da96e5cf4234ca33a9f5db439fe28a5e4e8dc5c85538a1af13efd3d0996d8f783b8388c20eec29a4c51552879cdbc26f2a444566e9b5be93734a983e1d48b947808d6801039feb27bf5cb5d0e8800a0401bbe5cea1b4aa6f342454e546e17537efd54d23d7d5d6ce735220e7191a58de831aeeaf6147326f56114a326179a0e8ac03bb3fd46e63af87db4d82f4c5efce6f048c6f986e0b2cc9ac28abc96f94c75ca27b43817314bac2972e5a91fd236f7d2b68100e6ae3ee6896cad46875f3153ab3d64001aac68a1b63ee1e4d7c53d64e692af2768a36027a0007878d8d869248e7abf58a69065625880a50f869876a963281d3e6fbefcc1229243d051433413d270eba78f40b3a103b70104e9455aaad7903332bd19ad90ef1f80019a583157b04e0b9135eb3abd299f2f1ff893faeccad4941b5ab81684898355f4e0526d7fa4d4cdf0ab05c6439c4a21b7ba8437f72e2ec702fed67c25e581ae1b8c7d06b782ae2d40e7ff4c8b2313045d0b77f1f4f2593b37abcba049d57a18cf3df537077d957bffbe23d86bd0d2b289ad739141e3b80824d92c6eabb7adf490a9134a1ff521272c8abe307eb2dbbc9464863e2f25eb333dc6ba084f7f17151711b75676a31189844d1dc3906bd8a26397e919305a303891b1d805bb5f9550bb09449619de8a4cc33213112442f9c4305784a3542cfe174361c607fefa1379589d1a37d252daab7d7a67d407943cea7c1fd40e51b0b19bbc9136d649b63581dfc47fe30c553d9a3c01e2bf306f8ce5d92a92a0d70f6814b2d8df4d0b8877bb58487f8addf97b7d925beb45dd913cb9d0bd76e287f7548d5bfc5bc20e72ad82a4789d451031201878f25199a3c2fa3425ac0749ea84ddfc69d48b132d3b1ff9e6b5dc44ac9c895d324fbc36e737dffafd53ec52981101750e0b6b46c975408659367661e246a1031f5fcb175a343c5a00530407c08676b18c2d8690b8ec0a904edf74b3d60cb52acb49773f808d878791079d8c64a6eccc1cde69a5e633d25e766a469a848fbe724597f80cce7f5f322b18782f2fd9812c87b7444af2fd761759d38f5f2d1ba3e906a9f6066eb4b198335d291acf3328254079b350888e74d108044a2365d9fec4f2f2ad23cab4d7116f328e2e0c09c88a0c55ada580c07ee5fda17972198ceae58cf955a358a240e6dceffa12f3e9738793cd55f6b6a3d48834519b7a15613b8120cbb2e384922b55f2b2a0f725b0005678ccba57a609c2b412eb77e105526843bf8e11b77c54ea53d15b3f39bfad1f343b9e3062883bd0b9c162d35b8dd37ecbd127b2e320592cbc7f8709dafc39c2c074f069d32a7916c55364bc957deb18a15a822ed08ccdc1d33a46d2b9036bb5cbb81a0caaef4c2d829181ca3ea9435475d420a12975631282196104a96aabcebefce970a1af66ad6ac8e6911f79a5b33839f023b01d78f45bf773cfb02b8180b7e15f93f4cff45b821916367203f34a09bf7260ab411d75e429e24cb3584ffae6e324d216e9bb766c7013be8b4fdd39fb110197fa86f24743ec9d4228a125083b2cfd917bd4545581e7b42c5dcc577d65bb2c91b8b4f1eaf35579a7315068e3e063117d0dadecdb71cd2ca913f1a7f364a22f48b02f384a6ca34134a03af7dd1a2f089bf543f8baa098095644b29f43f533f797625a753ee8dfbe2c4b84341b81d5ad0e363b2e2c1465c7d4020bac01e385692f468bc0305b02dba10da4a290a4e5513c914c578bce1dee3e44a7de8abd116014c31744b4755f5e992665e6095adb916c110759a1c1c936f45af31d03242b49b4bf94ba4f75e999ff477d9eb5b43be1be7b124de7345151749ac5a4da5358562cada110dfa4e64343d9a5566cde59404cffd43e5f69f8f6f87686328872c678eef93786527bcad4e5a6753f7375dee8a70ac571cf4f55cef4ae0b806a1771b3f14e1076355c937dd251b944155273d2ae72c716f86e27818b1f9348c983065947c82e7b9dcc370e7d94987cd166a321970b11e6abe055074bcf9438a643fe7a531aa2159ef276552ca109e62707e0f841794fc23e0959d5483ddec72cbe11754b49d6e09b5fe8aca0453e167c8ecc9b03302ea888af94654501a91af37e73394b0e41f10b4588ea864177ecf48b5bf2a9a57a6694037e22b1d5e51339ba068822e51e2aeda30e9f39722fcb5c2f8d939e4dcfec413922df14265747c2eb80ece3af341ad359fa1f85d7fccfd2e25f2d00c8c85c387896be741c6ad8a8d4a3ed37ced71bca23df5293ddd00a4c9ac2f8ca0af7c42e937a0d1a457b2e4b145d634088116a9061795bfca6142216a9660570a1097f309893f53097480ccb99978789ecfbd5a4d007c2cf21e48f458f1a6035b4fa38790993a966ea4ab18ff02281dc124cee71fb1da0d25951db8755f328c938a9de45be356570614e44119798495108fdceb16d9255d5400bb0a744b5fb22ec4f037de55102736a4e7a0c011384a4d138f92c71cf39274278a63c8620fcd8cd5eba08d6dd691c54db8607c675efc79f90b95e7464707f0acf6ff5ffefe08c180eb73a91c68cc0c20bc3c8e56f9c6e51fe91e1efb4238fbaf03f2b03e086c053f8212dcad55827957319029f338d89d66b85f1144be24af8e4fc3cf14cb6132904fc80d859d8d9d97b8ad3b907623a6771cdb6b95ec222eff74689922b89358cab44fadbe46cc600bd9becf78f7d2018d46c8f021cd003448c8c25adda518d99bf60bf6d1e6b8233958866127fe74bcff41b5f56b9cb66cc966583fda0b7af4cd5d80beb3e999ea8cf4ac93c86e597ae07e454d1690172613340dab9f09b5125467869138b8bbb9535c66113fcfec83a05aafffd71bcd75ba8c2f596c3d5770336dfc6618ddf8294bba499241f6cbf0c4e6964ecfcd203f146d13f7d26defb82af170a63dbbed33c70d78368d5ad17019e00d059e14cee1ff1dc11a7c6218b4870b54f5cd8fdf0839730a11f3a242c226a7e84cb61f0bceec6907843feb825825f69583e6d5d4dbda75ba47ce5a686d409972a72794f83e4b02b158409bf1251e12bf4f5cd47c01fbb0c891b159ab521f8a0a2ec23510e14f2eff7e1b3679b4a39d9fd1748a52bb72f72cd94d2471e099999f7de3e47bfc501ed0b8177d78bc6b25601dc1aa9545c5ba3face814b3181b58b2852d2c14ec843655e881d0c86857dc73df594e578cb5f1a7237ca70039c320b6d91f5044df93c15fc70262605c22f35280453a20ce28fb329e9c9dde549b40c86382bc88d0a1855cb7848e7d8038e827fb6c68fb8aab8bdf43d9912cf0f15f2359f4c8ebd227db4a1ba5d8444d718f6b2495299e6c9b8610f2e35a7d458f5606433968df624881ba643e637809c5a48ec72320d869cf08c0cd1002170ad891dc064b28e748f54b6dc64e1830b7aed4d287ff8036dd257f13f1f4faebd165604637178e788004b1c26389e9566ed875fe4b8a2d9d4f2f90c49d0a84d7247cf73776e5368a7072da7e7d1de995972afd4512fd6dc7f49d0915700ce2d88057cbb2a4dc7f971037ef5af83886df2c9b8c2f46eb9676f40a0b07e6db0fe810047985a6752d9814f87b5a839a661bc408c504740207f7400ebb67fbe33f507ba1ab010c23e39074e02a4a69c36cdb395493b3bbc1aa4583f26bec111e6fc5118c23b7f448a1556e665c5f1f5de41451958479e9b910138cbcfe6dbd5c9603160a78707ec4eb3492e7eaf03136592bae40b9c02eefbfed62d03382a0404cccc86c2340e4880902e21bbc65f12c271be2aa6ea745b3bab3d51770c59f6d557085bd699c85e428e0238403bbf4a63a32a1493050e288e5b2972e734daa3479bff7cee8404759135aa4de8409d77fdf45de9992523d1a3a9d6813d3d63e7b5d1317860c9e75d223cf031a497b7e10cf1aedf36bbf2b5b57dec9fb0c5a55ae0fce8e852c60879064afc2547921306c5bfb36d42301462d5e43ce55c704fc153c22c88dc7fee473a844ca8801543adfadfccc7e26cb5c79bfd3692989656ae7b8a86578096aee34bc6889ce9e7fda442e710c29df2d93178a289652c4477ef9d0f4ce6e9acc1d8398e9c0195678b094ebaeb263db2698c2b437413216391187df2941eeb00842b833800d9776bb07d6a9d735564ccfbf99f78c92a4e85288216e35f25e483ce331c7d9c96c7e7e97741b6fb3276b1f3475d77d9de547320588ecf455be2cf8e30321f2662d691981fb6a4d74771b6a39f5697b9316055a673bba4b698384ad3e4cbd98b40aa89655dd4b5549318f016d88f37dd0c4d2bc25c71ed89e4b62ab37c6cbf3a95f6a97092f74b6dfcbe224a9d9405f98e0b6f24e00e0190058d8187795de5e8c233ea9f9feff8930dfed0af22e49b2975e48527fa201f7a2122c5b61279e066cc520ded2d4edd9cf2e8f1b0e83306bdcb4755a19ffcbf48086577f68096ed9ee937f4c9bbe178afb633026579fc2b122ef4beb367a9ff9e24660d850f27808a8e0ee24f5b902b7f58553f63d79b796eb560b3cc6f6847f9b4d4b54d2c4f5584ee0f1697827f72d8c67f766a3554919091a64dc33861b89f5875353e36d46fc2b0a637e40f58a56425f7b21e276df5c130d2220dd7aad61344eca946ede24361d0d780cec3844b47f2a8bfb3c1b7b415e7c4a4db5ec175218fcaef0fa4af217a747f474b4e6bc878c0f30eb08f0fce0a4d292f6cd616061fbc6a36524a4f8bb8ee488cba68a33777dbe8921634e936fee737d472c5a38f0c33c5bad48d9aac0d93d533f10b053a9565f6f429989cb2f537c9c06564de9178f1455cd36bfaca268c9440e0ad396661c8429d346fb9619e230e0859bc073c97dce3a0f68efe08fb5eb97135ef858eaf241369c2469857d529f1745cf8b81b79811f2e5645e911cc8b43f667fac48a757cb67e5e3e587e3c11e29b224ed8ff6f8e45c14a0217c3f4e6d3483cf19b3b087306de24ee0666c93fe6e5f78cd5d640ffe3b645588ddf7aa4c9426f64e3a91c509e8b4feaef3fb9adc8b099e968abb6a516693b4852f05b01352aeff657d246f1826a49c08e8e237c481c9ff59dba57fa087d1ebe5448afdd8c401c2a1172293bc73564af2c49874bfa67bc83044c44ad45a5e33593b6fe66f54a77ae726ea25bd07cd9cc54398e169a5b23df79417900c47efce99334143453e3bb524d29d78b27b717b335ad8edb9bb82f9d619482c269abbd48f8b5c23f343b61e7cd3e5ef62a9f40a18cb24fe76c4855b31117261246765139d7be701bac422d79ab7bcbf7001d5f97e515cfc63a53691edf626cc20d66a42b89d42fc8047fb88d26d79acb8ea363e774db3072c3ca2d9e3ec0224c5144101fe89ca28e5e20eeda1c318c0e297527f7193237b13c3fcdc237146575f1c0b539e88527071b9a301d9b4d3697fb14af6feee586588603a8726c50ab0744590bbabccc909d34889295b8c1f810d961fabf8bb005de96a395e342cb56c813a3fe453f6e4e21cc9c04de2d0d007c155ebc2cd4e0acf5080a349057c878fce1729f2410e9547cc3285809f95c622392d80a6448b4858d42093f623974def948f4a000303c2b4a27a56d29e36cdc7b75a2cb3775bf9770ff2263d31f2a3d4b32f1c76543fff80c3fc9b95f40833efe6bc0e34e988d926ce91ee7504b74484ced894bfd31c140900cbb4c40868483606c6c02c901455ef54811a61915fa0da06f1327aaf0f5cd222ba9ac20c15765584b73142ba84507a0db524f7b038082de633c985203399a7abcd931dd7a8256969a2c6a252b379fd80418c1d5e67aa3a6f8100aabde37e69c0d43b189f43c5d84b453981782d4cf103165cf02fabec59694c5470515e47091b851a14331d9b5a513b48585f539aa9d4a374d6e6f5cf5ed61655c4daae18ab523af0fadcecaff29168a8bb4cf79482a864f3ff69065e0218235c600746e8541d799e653e7ba8d127390315a55844114a835691fdd040f5c50a306d76662e451040ecf1e7d9529fc57adf4fe6f15da85e7d387b202029ac1efeb68ddd116cbdb12765227be5d194b2e9b338198108b22cf81df2495a0a5e1f969f4501c496f17e28d936518bf917b43dd9c9b77df79cf218dc4801e7dc434e0195f433fd4c512dc6cbe618572b88bd878d801f5ab787b82233514b87ef41f066b1092508480817d4532b1b10038059b9b603987b67e685d01ebc8f8276eda9ff19e56059e686377fbf539b1a874b5ca83d898ec13f90e28cff12a150d7133c6ec8af6f5de477e1a0dac4ee4e582a962a5177d7da9885a93802b91399c3db3a005d3b06e0a54d1682992a0c87710f1df3aecd88a3dc56de80148d4bd2a5e6af48b752d9f525f0d0f6ecf77f8a18887b9393b8c7dced63cf61385c5dfe2ac9153e46764444c5e2951cc284a6be30fddc42735efd4e8f061a6108250cfa1a1818fdfbfc57fd4f1900044608678c795a58712ad2d53dff8b78e5d5991a817fbd132ceab9bf6e0e7e1d75082c6fb1cf04319f15f870dee62fdd1244d0c4f1edc5e7e777343fca8bb5739acc45d730fc271c551b0cc208dc32d8b8cc1708cf87a03c1b66642cd4c191cbb7971a4322f08da6388df5e6de0b3c996056d689e14b3edc800f09cc4716d4ba8017310fbadac1db9e8e7284babb297192bafcb16b196a7ba7042f2fde9c3d79a1bcaf7889376bd394ef9162c8afb152aae9585a8cfaa77f758a3921162fa7da9a32be3a85bd887cc019dd1ec29267bd342558c8f128eb1e26becac7d1a7beb72b9db2fc71dc342429b52846309c96cb82e7ebf3a12000fb510b1200c2298445bf2a0f85b5c545634b3a979407ff0999de1d298dbe2842bca29dd2d0288cd6260e556ffe047dadf815e889d970827e9fab77ef720e5ed185e97ca8b94e51204d64abb68198fbb90ceb7bec913771d3474d0482000ad74184fb7b1d517fb0422e517060c6e777e811546338508be8e53ec9bf12e8d94dd36244f6e9e20d1ef713ad9f0af37fdce32c17724b39658473750274731c9a4e81e87c0f4723c9de664abe03e1e7ad29683af149e88312f8d60b2ff722b8e2c2a0b372c90a95734717d62b79fd8f548a59b87039c26e92786bffc4766c16fad34381925ca0447e479471764ddec5e6c2417303513a370943ae83de1f26e653a5c1e73fafb863e27216a27f30390143778b75f4a2618124721cc724d1a8c24d131704955c64de21dd2f40f20cde81435c333293b3c78738aa489ca89cc96651ed48d2a297b10e81ddfe2ac867ea613fece4a0bc995f572d80a7fa7e3b29fbdb8baaa37da64b22c03a8df5961141d28a7415552270e7127364b19ccc3f643487630d920d6743287c7cf1a63042ee40d24c156c029b73b099361c308a190640c7a19d0ea2d73ba05fbe67ca3cbc8652e80a27ded1ad91824be5c4eaae7a4ecda6d80669ebc2bf13e9b3e6f6212155462683e33b6a2833ac15f5f0b0e26b1393ce3cba854494cbbbea052b5783e4d821675fdac047effda9d1e276662ab5f03dd5d9480408c44f0b5df190389f4e0835ff799097118040bc490e8b9f5ac60fdbd8718333e56aa97ae064df3460d47bb7133e1edb8528a7a720cf6511828fae5143b8c7cc14366443b3236417ae38e977336163158cb3126e3f47330f3f39fe350825cb74ae10759ed426afb1011c180a79862890d859402331483c46755305184f6b8032e3aca835cec19e6c4800ecd156118f7f1a58689d599cafa6e67f2bbe09462162efbb2267361e8b9e0fe8bce80e969bad4c154518b60bb913ef9ce5aa1d4f75ecede4ef64938613d3c33b8ff22c0753603dd6f3590072da99e2f04bccc219e3b6fa1dd694e4e57018866b3208ccfae7352f888f34157b427e2c32c5a76f2d4cd7ee394992628443d22186058c819f604be2a9a982775541eee5067e1d3a16be97d275679927a02aaf6d6cac766beb078bab6d0a9901f30afad1ce2394380b56e353006d6ef9ae4d1c59a563f1f75765343faecd6b4d18aef3dc93ad4234c3a677eacb364c9c86eef85a2c565982a8ef672e7c74c17c6db924cff19bff460613e77d71065f40c808cdc1f4a4292e7f5f17ff3df48a214d52e40fd65f014de26c0b3cc7e1bfed4bb47ca6592ad935b990d35b24e1af2e5a93a05fe966e1c466e2105f769020b282c32d2c8870d877e8f0d1ab3953488300cbb33ec5f013c3a60cc62f5c40a29b271e7fe6eee5501057c1eb6221c79e7ee65d9431d3b06a264c79477a9a0938c46a2be6fb4c12e3d4f713589beb7e18dc5682ef277a2ffb56a8c3fd6246d695abea1681727eb09833a7af6faa3b02943e00d0bd8e571fa6cd1b6b276cd73f6fd373071f0b911089edecf180a91ab76171cdc65fef9a63801407d61d6b0700f490a7b7ae387696282e29c325e5156e6ddc811d4b155ba57e26766b784dc8a6573b1a33be9090d7b77572acaf782c033569e8124ffde8798f2781b11ce54ed65ecdd3d4d92ee7ffb021ecf35163f42ac6c76be60ac1cb01ec502ba506f611f36228fc1e7f3663cd48e32586187d3f9687298866b3b2455387770f696d1868f795012527f8375b35405a8a874199f11e8f447bbc495d9d47762590ad64e182c7b9e9b331eae2e49688c03ec9a2f2ce5d230f4a1832ef15f53b4ff5751bae1fcd87e990ce33b20f547933203a361c6fc633116273b8e1bcf3b6d2fd52018c97cb97cf7bcec8c53d6dde72ca01f009e6cce10cc3ac072cd1c53babded28edfeae9200645ae0c54601fe379f620256001c2978ac9880ac0d4daa3be7a5943a374586a2db6351a1237e471db2d5eeb9c0af1cd33c6605fdab79974eb8da42f5849c8d5523b8dba7dfc6e6f01a1e975aa7f02c9378fb4f64ae81be12c277a5f6a770bf3a6489616df6ffbbaf102b5444f590e9a973e9b73bbc824afc07888876fb5d9efd59f8fbc541fd2b3cf5d566ab836b818dd8d8d87a933cda7dac8f047f7157646b519ac61edd7a346405530e5f61b7ca85d293da3c2ec0f3acfb3203c40119a628d3d9b6adef19d0b7ff7280e8b6b03a810e0967245ee3dad0adedadc7097d68fa5c167f884b89d5cbc13e9f59c5c4c1c272fd39b8c8b1345ff88c6892460800fd3c044322c618580f6c2a6651fcdc774cbb3b3772a868933e0282df4046f410c27ff761d3cf45ce4bc740cc563a3ff31bd877748618231d81e227e442e342f9d56142ba2594c97bef8e4c53e3874b3ffaf1770e56d703669a573457aa8a1814d9efef99788f859f9fdd68be2a1b167b9db5b7e6603132845514f1a59901f8fb6fff9d96f7b880b1026416a44698065615167e536c250b62a29e494721ad78bf728df8efad6ca58f1e432128769d084057da520cb52340a8ec0a95c45b9c2d5cee38c767bab2b2a5e5485c2f908b6646704aa077635491c73837b1d7d1a9f899d9d9eb0b9202c6388f0029bfaf431517f31c68ad5d2d8afd27eea053933d1b92863f6dabea9372a687d7b0e4bd1fbff6ad17db0e3932f2adb1dc4fabdc1712c6d80f1927226887246cb678d291f29fa5d8030e16493c3561bdd23dc198f6f69f16d0d9807a1d5b25011c8891c4c81e59c413537e7da915c7c34599af8f3cefb430109a157c1d0f771ea9a3eddc7679f6e90bbcab33fab5ccff2643256841ce080e15ea06068cdf651898603443a9ca8c43b908b1713c0e4a30e1b2c20763de4f1e98443054aa4ef386bd859e2a6747462e532a71341d6adcce04aab916cc6231d9549a27f5f069dd916b4ff456793c766714c62ee86b45756c74dde36834c436ce25b809bf4826b9498fc8ec5465a0b062045fa94bd21381784cc666f9a0a910d185ef36849b799ec4ed45a7a48dab5b07907f6e00b5328744216a1d5bc6e660e8c1693c6bb1804284a9f0fb6858d54d2274de07489404484552eb6bbc2e10644f2b16751044db0b35363b4987e4bfa6aaf0a5e3583b4a01c20203ded5310c9c3e28fa2ff08a51e7ff1235dad8b8eaf48e418b42014684c9b53fa00b945ba255f4b663e622e88514d71970236cc2c2f72366a78dd5a8f0f3a8d069ca54619b5bec51c74e286ed7933d1b4fb5514ec00299bde729a8ec768bf091d8d204ef8980605395a9e6e96e4f25b3760f4886b3679b2675a355c8a0607b8758bf3dccd1529531027f9cd0b63b7c9580bc337c431c2afb2b4655d3b87e0ed30bdb6c5e1c8331f016d991a334cb331e4264e54c8cee6f30673e678b609af7f2dec73aefad018bf8e96700881b2b889824579091ef12b6f0769d00b317e593db7ddd4afd5c0b8e89bf4087de70f1126d4504be06065704b2641beb4a57025b16715f6a049e5e6d52b07f3756982a2d46b6d5979387b184a6cad45b05890ea0958302dbd6cfafc19e2bdedea714b00cbbe46e560f2c4b4a0d6809436bdbc688a1437e2e2772b84660936abefc65288aee2f3b7b4c88b440188eed6275158d45a4087c59bd0fa6a8ee03e6ae8863cf4d9f510c1eed43a21044557fe943e7a5cbff1a80a34ae8b4e64fb5ed96b8e0dbd1b8ff6993aaaf11267a4c6152f4e5be727890a37640caa7595a1b50d4ba26cc2c0ec73aecc19d5e96a276462bdb3dc476cc85a41534c529b4281b160a6f9becd8d5c8fc696fd6fbc3e5ddd2a4f0fa425252963fa0c8c87669a3159bc859130c2ad4fb07f14cd3b43517b4c1c879a2757d9e5f3593b8bb50c1618e66301200baca7d0fb7e93f5b0431ef22d2f49b3fd86975ac050eed4edabe44f4121a0646d89c23109f9cbefe83c9863efff742ebd43cbeb20469199fa9c0524e8c70b7a912540cb56ff101c32d79daed91aaa4150ea023945540c91aff3dfc686893db23c8efcec69eaef6996ece2719bbd6168a120c6343ad9f8fea5ef4dee627725e5b8bcbf467cfea7594c19c75b9da30342abfd876e34d2c0eae54bb23bd59579e6fa5142ace0811c714afee405a69327e499f088bd002f297407e6690f8132bfb828a1c06634f0b3fb1acfdf51626c11c4631efa2e18d92b9f75d626c13e946973c5c63f97db99dc975a962ffcc9e871a81fdd5206f8d49180e1924b068c0fa0866320a51679379ba5d6a62a637c2e72cc0de03fb84394c7ed5e3b1ad44fd614bc20618fbd529ccc56a1e5f30b0997978ca72598d3275c047721ecf9fd8804eac1317259f6ccd8cc6e23a9de94e31cf4d770454acfe04f8ff57737c77538d807e3cfde2160fd837f7a33ffda6a5b733482b0c729a0f17ff05ad620aa0c46bc676e6f8c2eca7f61ee47fa66e4321e3051be62d0e94e93d4b6240115f1e49d30beb84d4484a3b01e4e8ee4ace2707e8b11bc547d54ad3c20bab82dc2e1fc53b7d8f94a281e907b49adfbfa9b162ad265e8e9add11297fe9dc1aa2e4313332a7623ce8636b74940f9c704acc9dd23579fd5d98576345ca04c8d1a34b4966dfaa863f9bc65d8d533e41366d6d205672e3f257e50e29a67543fa5747538ddaf08df7b0451c072cd2d50c998661d5c306e502fc4e83119448c58d54d3778e939efe652104881f77a5554f7f62f08ee04ec35cb5a7339faac9b5acf4989fd8402d5a06e807a996009d12f84acba7287a892a82040914dba1a9864dc47a8dda235109be39ef8a83ace2ef47ad97d92fdf614770d5bee1adf713a4e656a2f03c786299f72a74da7b36a049487131afbc3a9903f25e242351a9134387c2e4b60076ae04d9c542e77ccd530e66de4437489db4f3e7d06448776bbda310e7a54a9db9a7ae97c70110a3454900c12de1cfa577e4d8055f0cf7e34b5ac11447ffbce17c3465469918a0297887cc1184c424175e53064864e68d3354221050094b69b98da6cac588b1fe89099407df995ca7c4b251d0f2da1b06121cf6439493a045ba67fb3c5aa6f6d672eeda55956e341e3afa595ac507a553a72c80823250d8d163a1b130f13938afa17b132d39b6a71ad6b6584bb8d02789d87ef41ab78355bd4031e38d17b4b85accdf889cf028bf0e7d61606a1ef24e6c194da86964728400870e2c1bdcb76da90b75b4459b6d49b6406ec95b13c664ee63d7284061e81e65965121be5b782bbfa21c646acfc80b7d2ba4c4999c0acd9ce5295c3b6b743eefeb841705900ff0de5fff28b6690421900fb4fc765b3bfe931fd0b04847e430ef85c70b3abac403243e23f05f49518d0d0c820220db3dc657e13e6b0d7c4baaa523e65bc0aefc88f09c3b6084f73c7c22164864cf3162c0b3cd9277cc55ad7d26b7189726b4edc81267fb3d9913f6f3594bda59c214ff4e8f34593d1ecc1f87788dfe4482fef68eb4e3da2768e345c6d8c9744d136c454513f53af4dc3e4b31e1f86dabe0572a63571961fac0216c366ad51504619df8a229c9fd20efadd87d922bbc183d180b179d07bcdb1990f44447e3449451e4df7fb24ba86b8f5f66132a492d89b6e572c0d44178cac32742d6d1cbeae50a3421c0f3f37ffd25103104ff4fbde320f9b0b5ea299c64262a41e6adf162a8009e010bbe2804dfa36ca461f0d993f63da4057824a86255cbc16039e60f967b2f37653444d7c9927560b9d906ca6e0801b188a20c494e87d38322f2d2fc18dd46504662eb309ba17016e4f9e7293ca9ef22038ca0b31ab98831551cbe14997449769859e936f256cacf7ad3c9c9290a105d3f3392ce6aeeccb9162f70e39b97ffac60617253ae72d5f0a835ecc5987ac3f6047bd3dd3e74458eca7770424a4a051b86999465237b5899d872ed6e5460a74f17622b6b4d1bec71d21af5f54d1879895e1e50a23f8d8d87d9d38967273af1b4294d97a256f35fc7e64042e4dafa508cad6dbfc37ee92db1d9c1b4f40c75176f1686b33adc77d2db43d11a5954b86a82bfde89d56019570b107fc95f2b0b2f2daac7209c8aeeabb64d223340443ba222497a99a5fc708421c4fe293bbe1c1c7862a944d91ad951b729699888c854cfe06a1eb64f149abd4ca0f4697b9de4b841560b90428e19fd4e67f7c4efaf979d7539cbadda09cbd045d083e4d33d5392bc1501617c0f2fef0e702bf6b6d4db56019fc4a20a9d419afaeada503878d6482191e29dc2cb1a6ebd880f0641c979ea3ac240ea05c0963ec86b6535662a23c953facec364c68aa8cba96ee081bc3f6cb09787e11b28f48cad1c75f19e630f86fd21f94a6f9f0a50ec0cdca7f046d9c6bdf26038d3820484bf08105524d803278ba425af5bf3d1c21d11227ed98430421fa3304b29de28a893a57afc3504667d6cab0dd1ece7c25d9459d725283247a72e025ecf627caadd87de404970f14b81c153d0e86651160435cee803f648e073a3c0592c36c10a91640b2fb88c1c0822494b50a1ca67e289d757923f61e3a9f299c2d4ddd0ef51bf5cdd052994302bfd712bd0a8ddeb1b91b5f9cbc43fb037f62ee00c1949dfdc6795a4ad83bd3e76f53db2cd09be8e613bb1d9bc0dd5dd0b254b5779882ad0c30c0e3c316531ace264e7061b1a15e6042771d8612b60bc75fa88172e4cc71e1e4159132853f7e4560323dfee29c980a5cc614c44754b032f19159ee5c79b8be1ed104ba47fd212571453ba6d07275f5191fb1c96ad6c87341f7cd0ffd66a9a5bb9bda58a1012ef067c3a41613e1744b3965defe603e46cb982a8274a625a3eaf1339cc38a0c69ff90a04afca9d030a1cd9f238039f9b8d068693a97a67f6abbeb4b0b82a13f4b16a58dc48b1283aacbc232a86dba47057724bd64c3f84c68cfc1e436cedb107ee30021be823acf2e803415b82a520fc9807c8a52ff3f48155a0069282c51fca780d22638b37b81d382e450024726e7e65bd686e3826e2d4e6bf59c81c215998fb0b662ea75c94d984d61a2855dd9cb74dc0d5c85558cd1b1b09e6538cc5581c47b57e6c3d0742609017041a37370e08fdedfbde6ca4546493beb4e4b15a7a5318ce147bd98df2f79724aa7e240930e8b856d99dbe380e5325ecca52dbe229e7039b54ac0bb17005b14611aecf693fdb95a3379ee4ce24335a9e5e0679063463991ca85936147c5bdeafac90b16b4df0fff953a8ff4c8c2c127e104c3126ab5a20a161f5138455db90e1564e6824af99b4c79c1c3a457db46d001fe18a6003cf966d8aaaf8d3374ed145ae496cdf63e4a390f6b4de8e2dbebc08949e9904ef2d1735b8a115b7643bf0cd45329e1fd403acab128780af7f2cf530c9726a979700979da9f3bfd14a0a6502dc21d3cb2c495a26b45e2d9f3c4b020fb493ed0e5aec1871375aceb5676ac6e72960d73197c16cfcab7eea68c53b6d7382dec16b40ba6f9dd38e0da0ee4067f56c6568aedda2be7afdc95f6cee3bb559094416e2fc06548c9b65c84ba73ce688b60a75a917d1d0043d06afcd1dcbcf62c14489ba7d269e37a4bb5d40151bf373302e9d612dac02f5475475e285b38c5ce3e23fa130695dacef383406052e4c7e8ef6f5c88c9d9046e3c357b6bde8045f25cec5ef060952c8f24b5eb88345ffed6801529bc426e079c6eb55b50edeed4eaa6cda9dd1128d6dde35bda00080d8bc7e56d62cff05882aba5ad42d9b233ce177580c02fae6308db5889c648bc7d837be83266aff6b82b47e33c7ece61c37439218330e99c36c09bd7cf965b5dcb3b22c2140a1794e1e2d382c2423f9f12fba0dcf6a51f909f29d6ac79e2d56d9109902369a66c743831810482ea042469b0c40714fad60d73268ac8c9c789455a7718fcf0e1e2f86e72d991f3ebe5d8648ff8c9d53a920e9ef9be4080001752747a16386047240717a5856b4b283f08ea5643b9ab1f9df6701e617a509f017b5c6c129fabd8f97c288ec896ec1890eecc6f577ef8a74fbf14bf786e34624ac5951d6b575b116d60c338c3237f8ed797415cf7d160af8a5a3328ad2c5400f1814047980a9147a3c7820bb49849ebac89b313702ee7da15ed9724f35594cd279fba42bd0e709172628fd3458f3de02ffef2101d7acd958178c8fe16d8360e0f4d6a55421ecc3e68d83e319913cf9a374e215643f50db4c81dd63cc9371bf4a2026ae2d0697754252e4efcc87cf683bf9c089a85f80154127bda4dc0e2c18f502d429f1e0d1a80f8ab8ff809951c2a9cab0af456a86d3e27bf678953d0cb58754b5e0c61f07036465399fc358f172fe3f63ffac7011692a139526bf7a15e56ea4a537251fe9efb4c6b47520f043bf059dcfcceb1448ace48223e0fc8e1981bd2b3abdc700f78b20916d8adb10413568c30b4a3884fbbba2bcb3838fbe0308826b57132eda4be3e1f32709496a85ae8f2617b59933edf0f9e537d72c9c1e3aa143e0ab9e038d4b26b4200c402e7c032d790138e0563356832ddbd2ac777cbca3532da0666ce1bd973d9081f8e6dccea8e97d5bcbb4b42ee67a8aee6c05604f1e46efe7d74e00ddd7730d47fe166f67f41f4756c8af1bfeb152947775f767655105b64447c20ed66d146e7797bfb8d8835ebb7f41ec86b988aa9c2146a6366c62155aa9f29336099ff7a5b3742d18db6fff62eb6be88980c7435a3060bf9f9a1bce21e41e74be14e77060a24b1c8da3b52d05f91548a57d9a5df990aa77493f489bc4e4a1ace08425cd669d0ccc9f1678355e724a87ee981a0f586a31fb8bf8c5a9497819bbde8985eb607fd8c4bd5d004277ef353f59f941e7f6a74f47f4623069e22a76832b47aff0b9772678943e27ecf766b140f0604e8ec7f678843b77988d6c003d5b627f42a6dfe532f89ed17864b3c31f087e880e0efa9adb2d3836f0b5f0559c0e3fba5c64dcb35a00484f3dc403b2fcf81909a46b51d490de6d385381ba70f6bd2f08be06ab0cacce143b003913106db637a4dafc4fa8e6fa53c0e1bce3e30ff25fbde0f62a426daf3c8f543c49dfe0d3f72f777afbbc92595f5e7d1cc078323146fa3498c91303949a9c40689b1396a543de40042a20cb7c4be305bbb2dbda8986c55cccd8f784b77de228735ad21edc25f3f21bb661ad71b826f9c6f4b95dbbdc9cae2a89770830b0cdb32aef5277567da44246860bea704c51b3bc49d15e0809377753ea2dc75e813fd797f4c24728818250753abb78136578bbfe49fcafbc5a33b136297e3584d3d2110105fae100a973eb3a3f33ae01935050de07125f30a12bcf857faa2e2514362cf1f755b31963fc5ef7ccf02cf176dc0fa691d5e259a978142242766d2f95bd2ad634ff73e99a0f401baa39fffb1b16d09d4fc8a8db22cd1c5fbaa7fcaef7ff494fb3b234aa17640e8f9d64a4679981a1999bedef5c3163dea251102bd5d943782c5a2c0db1902afd9d48d4059d2bc778e36fb00c33aa4fa5a8f15fe318d1370723d4dc6f8f0ade7c848d019a093823711aab279c3685dc4f7a3d99a289ebcaad3aa3837a85491df749e29c409bfec463d1e8270998e99c1319210a09210525b4706d37c9682e62fbdc950ae9c8e193e2f0215388c4cec69a6ec82258d7cda66e6c2ea6da512d878572d3c670c11e4fc831ebda05ea1a04e38bce3da3c16de42854d5122b1397facd7917c707066b091a5e6317bba57f967b87585466d7703fd97ff27603d7e016019605433ae55fc2c74322275d37ae97c64a2f654efc7e340350ffccbe8c37b6dae2b1c688956a9b60a018964c0a213a9b2422d3bc6f2db773452471f46d2ab3409f8279b8c0768542d84b715e4eadeb31105ae6c8afc30c549ec159c90e9af7588ab89d80e3b68ac0d512b20f62421d3d4784f2b99154eac5b4ca9f8d74dcbd5f4a2dac864d45ced6fc89d3e08a30e12591611579573ee752532de66429984fabb639a6aeefc7aafde1ed4b3b62aed9fe85c52266d60e3ec52a3a1f870eb01ab8a99302bf3a5cc5522d52b3f3d5807ec4c50f197fc5883bd856aeba1014e9a376bb4f9ab47e9cb2aa8c55784a34189aa3e80ecb88a0cf892e3dce95915409915bf8d94e6335e68402b3224d38f6a0911e45d00a920910f01633268819793f6d60ff70ea97d99066e58f943e42d9ebae8a1a265f421af7672238b07be9dd5cdc91009e74e583b56dfafdd5f60cdc417715f317b4bce8c1384434d4410b2203ad7f6deca2714633eb2b37be572f5cbd8907e07be3a6b46336b145336cd619fcd8a773b7af2408ab2f64acd740189a437ae263ccff5439d2722ea1dad24ec3263f985077dc8a98724631845970dcab13c7383619e19c8dd5076cf182819eaba7546cd46b2fe7d2a369356e5b34f635dbccda4538bf5d8f5908f4952608960092ce42e77da7aeec00b74f46d5dadccf22517920b0fae8f61e02c418ca3729df92d4f5a4ef66812ec4ad8ab094d7685650de2d20cf57c331c1a5a38a1b4ed2087bc835136413c02e4dcbce74e39e77ad289a5802e399d801a14448543a0e55a79f7031db8e706efafa2f83cbfafccd3338945829a4c00a486f78e3faca3295c19b06f6b0ae26796a9a6474838c37f460a54b120003260667e4649da92f1ff9ec15e2def277c5b1fe4905250c187e75b45661e833fa396a583356591792cd99ac080ebf2ba14a4db9e7d6de012e7f722cd497a5c6e7fbc29003da49c55bd27a6eb3392ec12cc46267c20f0dce4be96f253352a65456d73e39a9fa165ee99154120b75d3b6c7b6bf15eb824b4f604cc8d4028b1feb9a67c86153a16d4bfa59447f1d8a2e350fe2e8c6f19e279d8d7f370cda87769f07c74087d572b51cb9db4fa919f77171d18ea782f2e4ab429cb7e8761e1b1a9d5172f6bafb3bf4ad202f26359de12e2b2af72acdfc36f2bc4ebf66fc4dbbae650a983e6908ad1a9ae9ba9d11b9536762cc42761b5a8383886cb4f0f820ae6e668fe181485ed1b7f2c27a7fe67f28926a8f701a3344ae6d00ad23ead1955cbc6989da729ba7dd82c116ad689cb98b7b2a4f480ed15fac254aab145fa9bb09c34799a06bb3e45464ecbcbb70d10b086277b213c21163ea7113d9f82001f15b1c852b30c8544ed23bf43210057f73ba8e07522a851024b0c065d54bde1b4b7cde7d6306ae0d3fd97fa46758b766d4818f17387858081a98e76d1b5e0d9e262911dd392b1f5f19cc5b58f9251f225787e49f490a630a9656579b46abe0554c66cbdf00bb976d596dae976f888a567c417b869044b59d002ae9de841c0360813ae394338249b28a10dff49a04444617c0789d1aa1255a1bf2fb809e6d74fe4b1465e698da386190e16d91839bb1f9a65f3fe6c1a61d393d60a4e79264f2f4ee1f026ca486b24cf69b771aebea0a5510311d03073bf24c6e1297059ec547bc71f42daff5edc217c076553bbfd6b20065a172f379512c566c77808e8a73980b19f666a46c6cd16ae46ee62434cd4a5e372af0e0405bd96e5a2daf70dcee29c00e3bd67b4b9418c80911048f7d6a437de664fcb3ad8d74e56975ec942dfb16005c078d26f09b10202a80e7ce1df6da3ca653991489ab1fb1f74cbf6eaf876de937ac4e0f4fffee2aeb92acda27da02cbe907e63c65a1c1006832d676352d2e366a580011b861c851bd9c730bec00d5840b81fafd191cdaeeac627216ef1e5339a99de7b97cc4e4194b6026bfe4e17d29a0af6e47f2bee4c3c54d5d7be8acca227fd3ba07501ddf19a6520cb1ae4f4a5b025aa79554d491fb35eb54c09d1984fc4c0f1aa753199e40d272eadf59393a78cb6ccab9495c16904b07391b9dd2e77770be0687b8da246d1c587ca1ed0b003dbfd11c512d2802ed7e8481865f46c240d565d2f413b4491cf10fa807d8fa96be6eb3e05afe2512ba0b29a8237730c7f8ab985f437cb348f6af407f3e0f510ef1c49d4f336e64cdfef2dc9c3c3acd126403b8df81e93501297bfe5bdb264ec2ffb510035c5d8ea5ce6ca258d6783cd4533b7d61fd3d4cec9a8e88bd048f5d12dad6529c7be97dcafa669c328cb3937d72f6e71bbf37a363ed926d0fe3158c93d4e877792d215651eeb1521e31fde00c07590281700e12394be5339d0cd5a1714745b7273f76196e9e2c8fbcfce5e39fb4b6cee6d753aff07d7f9c50a14230c031075cd85e90358d9d5a76a7afb1c3b3c00fc977f467b463fec25e1f621f4dfebe9bbbb15349f344eef825aaa83f0a745f78e83ffdfd2613be37ff04f5e8bea48a787f63d240bba9794155142a309ddbf49e3bac779c2bbd08b7e3beee736f8d355a5ab073c6876c7f1356fcef7d0461af465ba319e313410f91677e6c9eb07c0be69037acc728b0d5383e96e283c27d556fb52234ab90ebfc81ddcc4ae9eea4d8b871dbe865623b344e9db197c678915a1384d394de1092dc56c923c5c605d6074f5054c27fe7a17c8208587b2d340bc9139df28347f2776f0a6c2b48ef0348a475599a049812755749d72fea194fbd90610577b241d0869eace8680fcf9dd688e5c3850c98c36c925ae69925ece2f070eb8cb32e2ed5935f442c15af6aa962b7e2d01094effbecdfbb0698ecf3c8a8fcd47843fefbed6181adde027dfccf7f1abe7009d084e7d461309ce9be18981fde7dae5e8ab7410e085dc995e23c31f19e17c01d31ae42ee25fb74889b8da4ae85c3c0ef2cbdb67737c7be2f93457aca182ec4e519950c084c191ad8ed8d0cc34fd328ba29a5480352d2539a2707f1ffd723ceb015a4367f65e99479f604253b7831d88c167c14ea533981f11efba78447f21570baae1a351d2849b54fdd3a39008da00f19d88a1cc71f75e473f2922f4abfa8da1bff0e6f14d6001f740af2146af5dfdbea8053544c085a3a2e8dd78d2eea87759501ce06ba14d77134a87cac807ca4fc7e13459438a82ce82603efd9502fbdd8831ddc8f8317d03d935b2f8cc8705302a4ef4f68af792225c31e4a29b0ae4c9a75338ad0fe341bfa17000e712ba86297a310c239fe81cf01020748dcc22bfe156dc9a2ffb8e4056255efe34a3b3fc2cd6cfb6b980be8f9f31a38a509e59bb32c34cc8992bf4b3ef0be3208162dc25894b1d27dbfb3e6e6c87664c2d6d4006061ed57e92424724729f1d843180794db39c6c90789648fd726a02b57346d3d11d97971678ad51266af8b5baf688ff11e32904e8b35de1fa56d96037bfa3cec4e2f14f7886c2a1deb7f476b9f85b18d696f120221faabb8a139e9db728ea9eb37975f7356aaf5bbc1a5d1814f7b5508698ea9d155e392327b5e095acc98a0d38d6755583d128075a1fdb613ffcf6a1d66bb400c82c9d32c746587a262ecbe2c07bf4ace342d360b7807e9183f2b5f9178f489d35df6d85d0f59c3b0de09f78d80060843ae5e432229397d4ff3eab09209f5eca233f964169896b010b4b6629e3d38806f3cb298c818fbb4777993d1ddf4b8c72b7d17dc591760e6f85d14e049f28dc0ce1625fb870ee84cf6124a9b4bb95d9eb43e012d02fbc047be817550ae18574cd32c75ee69409fa028ad9cfcce1fdb05b2983dd642950d6f693895a60af8879142d184a84f59a3a4e1f8c6316eccecccd94ea0047599ab1f87c4e0272283f3aaf8ffd4c932d16f22739ddb511a35b4df45d389a8ceea243031a9264e7e946e996e13c6a62e199d6f084dcae1d9d7eb618d3596c1f30e96e7754ae4f89f5afbe112795f40677b759fcd4e18021f8ceba28993e4363617c26235c94c921a5eef71000f89a4a71c2a801b231626c3317eeea95fe917fdc4f762e2d75d0f87a1ecb5512c2e7a7241d378083f5f823193c08fe0927ecf1124ded02d8f3df253492b9119307101aba40189795ad20626b03d2470d7fc4851bb255f118a9f206927017a8efde9b8ef79cd9eac9dcac85f0f06976a64d8554ebc291721e39e13879e27c50e492310464765297a98746144a6a05e43b33df17e23cc53d5d58063daf414889b157fcd0f749acb7905f6c7fe887b429eef3ed6532f6a64914052facf823aa3a5b965f03d75fadc54f9fefcd141d26203b43c4a76af2ee75363cca19dc1b0891c8d09b2f68491ea9a79d3308d2ffed9c8a38323d02ef45fac5f42d663d05dfa82efc7c8a94dfc32196d4155dcf4dddfd3d6f709e54661f81c8cd4764902e4eb24fd8905a095f8a1b5c385783ac25f3554450f79667befcf4228d5c6a2f7a06302fe2e6d5bf8fcccb28348af8b60866e9b23928062784bce345bf2f420b322058da43aae8db3512a79da279609558411b0b1f844bb70882735616765c7e2fcdcd842adb95d72c1b5cf65bd37a7b915ebd63c13b8d7ff9d82d41bc6dd42f7b33031f8c412e5790907ef7711484bc2897e1a748cc478faa4199c80f2e8034c19e621d17fa0c0258610847c582e37b0e245fc6a54e2d8207d05737a0506fe80ccb6c0fec881fdef54f83d3457e2a273d89eae539b3d5a79bd5f5a61fc3e5d363fcd0b7e1bf1231ba03228e51c88498365ac4aad23d10640207528e6f3740c408ad6c3f2b3d790a3879225855b9bbf15b89f39e1a91e155de6da46122c16bfdff48d059763f9ee5c22f76bdfea41a528f046534f8b9f81610967a7fcd738e0fcb57a43b5e3bb71db33610b06cf07bbcbb50130c9f98d28c709c49ac4b68f000f00e348e86f1bd26ea1613edc40d254d027c9464c88c2040446599d6b5b9437b0bc01ba617639a195c51377a395c1f44701921dd448cef955defd03341236d4323b14050c208ca6a562d3bff5a66f1b7c19374f3282fce007b82f48a337997a4e635378da54bc9c578be85c10bd52974fbe5bb7aec891d250188569afccd06b3697727c2004004fad24647a13f6669863f2888d781103fdad871c886cd3ce76460a88a5c542c2abb40ef578ff7d8b69225626c068a26c1164554667c52ee41f2da533799e6b7029b75dd292d9a80ec7940c605af545e5284316c27d4b9f2967bd89c1d268a60ebc8b9fa330cc72ae7de92e7212090a41c8fdc40817939f658ec2c388d324eb328cd2d13bb3a3b05e7f87eac6dcdf8eb894d575b582e71da6546fc42f05c311cc19689ae815ef74f45403d1f897061105b6bf453181b7a9ca1fdc2733e761bac6b0a3be6d82fe5f4cf7a7017952a2a31bb02b328f3d05ca8cddded7b76ced687ada5e82f5def17a32af9d696e06222c95e7982bca9c5f12a2bbd9a28af665c4db4254a573b0d93456362f9cb03d5c7ed12d4956d619134713883994f64f99629fdca0b0ad9307fe70224ad5a7cc2a864bb04b75c91c2f95c7328cae9b2f7d786dd7d7dd40a113ac8784b7ea058bee87a48b251ac53639427dacdfd11dcf1fff74969825d43e6603672a5a264004ff52045ec959938b03101f748c53ae2d64afc378d2e95bbb0bea447f76f7a4e564ab67a57dfb77799027b9e3ae16983edc75a713074d9d3c9f731868409bdaf994eaedd2e7b9356429f6a14f921d2d8fddcc75f59f958d37fd87bbd623a15b0894833633a15b756b860afb34a20f84b029f90879d193d397e05cc2ec89ca5cf4986f0b8ddf15f95e83d197398308c3de4807392a7db7ea3274dea317fab1834a2449d44f723b9500f374c9d2eea30dcfa193b7b9a20901e5cd79a0037371de67f5757fbf539da60c992c4c3e6d5c2592d4ccc748dbb165e61ebc91cea492aa0190506dc05fb84f1ba6080f75b9a68e5b2bffa64b1445ae437feedeeb20b5134cdb18d671754b2ec783437fcded872d5ec8d1e3cdc0e158bfc40cc87c8b8038c592ad3f1d9cf83886b50244eca4bf702db574c2ca0746462f6496a22d23310505c5fd29dd484094ebea387a45194ef99fb11ecf0168378282e55a5f4bc2e19d3c5b6283df252440bdbbc606760c5ae9ebf4ace624c280a42421fc1971c3bcb4b3a2adf781bad2b6014a3e5ccb972666f1b0914722634ad2e4f895993c1f3e0102036c01e68a9d763908c3541e793ef2d12b7f3832999e127457e144d68c066a33f4d1e3653fc261f24a3b4ec4cb899595efd277110c6c602831e5b2f2be6598bece09b3c777f1651e33e96710ffac3042dd2dc3da32a2ae48442f458b4164a7c35d989036731c16780e4062345b73a38ced5a10e48cbbead1ac7653686106069a4e4d568ba6d098df7cd9a931f486a1300ae45f919a67621f95ae2d3b791dfc314f170bdc55a0bc5c70c8129d73298f6f468db9648e33d370dccaf1197e64182b0f995526ee1e39bc18de37bc8a152486e2e4cf8cbbf8cbd9c28889aaa9af72082b2920c2bd6ec3471afeb107ac870656fdac13bc666e3e2d3ec7fb54398ed88167047eb1dc7ce1c88f470035d9124e1eb9304f6a83b7f4815593a57df72a2d0c5580fed165f4adaa87eb7e166173e7411b67049e73a8c73c75ef9f81983c09c7ddbc4a47c2725082e3a041d91b05399c9785786f5daa5aa79ca0748882910e2ef8d438fdc915676098e70e393619fb3af2f72b3363901d2d8c258bf100cb39091d3f9250ebc6d384f6425ae308369369f0ab99b7419446017aba3bcebc5b81f06c1b3351ea141872ebd311170ce8c2b6dfeb556f161ffe388735cf94acf0a341ebe60c55e4635a95a7d134e9732197973e39e7bc89f8313e46ef67a095b946cdbfd67e4e02c66d1742f3d8ad5ce1086f17de608ac6e614fa731890a16c497757de5ae5af5a17293b0f3db7cd2b0eeb51f3bfbcba26631a138643282690d5373e9b4857af0034e53cba08c024e4cb61c48e4cbcb7fd3d29cc1715573e6dc108076e72ab6bb9a6388754d20a31c5d800a5cddd64303d2f9afe72b51211644e1dc939a26417f4a8d226c9e5bc4242c291e342e22f48e16d528a244bbfcc6772574cae5e95bfa12e4ee1019181806598dc12136c2977ca5ed4652744f7dba9a3d2d83c89b4a7cb59afdce3010779ba7100620343318e035b054ac46eb32902be94ab1447617bb925c861a86e9ee2d020499df5b41e8d647370ac7891a04ccef08d81bfee79f6e0fa7581d6678f6b42bead6455b067868a15309301f1f7cd9c817f04efdb5192f4f6e10032129aeb39c0a6f328a83d30c6a4cdd7ea57c600fd2f410d4668caf91427a9fe65171fcaa4644eefe92de69f45d2ce73303b53f3f8ce0642dd7b9c67fa1418ecd111b276dea82788be3c6e145b07f70497ae75a45255e4217553d60f534220ab1260bc4ec548676375d884b17c4a1b8375f14bf66ff50b90106c5eceb0f0a9c7940f97101fea80a0d45c0bb61a8a8c9a17c5050ec6eb5066484b64d86d214d5343bd5ca5f4faac56a082c09d35a4bcff2440d2b8f72edf5f8d95409dcf611235f604d1e2d8c930e242ad185207746c343414235d7955883f109062d075251288450982e4fd2abca88e418a45d00ea194587ac6aa283a2c8435b1456ab1caa5ffec2e67c8c27acf3139c0b2e23adbf35dfcd9a265a4cc3b94987c6250f9490a8cfbd8df21ecedbccdd2013fce1ec35a7a430aec8213d990da9fcf3d174a845edd324c3db6a029737288cd971f981ec11ee021b086bcadaae2a491e6ba69bdcd30d917b2ebcf085ea8b5381dabaa8219fbc39127781abe66542edfc6c5d68812ccf7b6a0310dd16837668b58cb22d70f20717df0b82ac55ce9a3d530c7ca03038bf1b0038a020e441c8d606d64fe9ed901fffc75e2a735b1f4a157be7be3f3744b7b471144b654565cbf7cc8d7f8f705b5d7acd6ffebfb14c2b2582fd88e90555242d4534b1740dc9f3c3b432b47caa44f0b67552f430b606f6058d6d4b55d71b2adf336e8cbc0d7c0d22e8fe8c3e4428e555140a7034eea8149da2cdb85a9b2ed0f73b1a855735c46b03b034a6a3fca19f97551cb848ebffa7c91b17ae02260cb9893c4d6f6ecb9463f6d5ed492bec7f6536a44128c1b60829b4bd195668555464ab2cfc1bb8a3a2a80626ca91e47e181edb4d9125d143530b01426a7dda72905bc3d9963804883bc0963f07cde3a8d61b96d9068667f44071a1f2cb1e637196f7210e7e0eb0bbd34d36002ca9ca2f0b835059b1a3c5b4ccd315cb0e54ebcbe25582d56ac857fc8d62bcf1760ad0cedb3acb49ab73326bfdad6557082c5352ee158526bee4633641197b1b439504188682b691ac4ca4f3cf1485e38ef6beee4d5a1113363060fcfafaed95bff783f5e566d3c8beb1550836cb6689425cf22739e9ebfd9889add04d617908d821fce782583761b005402911d0e07cb0a2e63641b54762ce6cc05f9e97caf3d094b5cb540bf6c1c5419d2b776998be11948dedf2e2a29caf5ef4cf083bda5645d1dd28d3aede649b8b3e5ce7420c2ae5c3859cd86929de830d6e7f2875b4e334dc9e5e64aeb17d5b1ab2d393122d06df1051641ca36df6097b34bd1a19b2170e25b41e11461320a33e428539e66687aef7a0b751f79e9e4440b74babf12db0c09541b3280ed2b42d315033c286dd703e67ff7992581d2267c643e207ed0812750811cb390aeeace3974dbcd57cc59f293d7437e50a5fcbf3045fa268b5d4df34cebd74a6049aed8756368cb39399e5bfc54d859f03d1d323574c3e5a732c0e7f297f81aab1bc86ee001543e7d7f54692c50a201e3f6489212cd006ced9adfcc8110daf4a0736c1d36d411afeaffd939b2f8fde449ce5f0f5945232baf049e3a164454dc1a29a1272a1d70488c9982906ee28a8bce2efc8f9db404780eb4d725effea12b2edd566e954949540c4a0f1336706f5471eba41cf2b938614ae0f53113ca8def88204ea2371cf323644f18bb89dc730f2a516cc6082d38addbe14be11e8a3a9f384bdd21a601f9bd0778c5f324ae32e4e71040dba3e8e8210aa1599eb5ab0c93052c1d1df421c9765780993c6e9ee8b48dfbc5094efec398374cefd482eef461accf1048f4d3f1cac5a4f5aaac1e8f93617ef16508271349367f86bf052b64d3e24ceb3824eee6075e4d0b534800bd9fe2b640643cfc401f49a848f6b7a6b49801a38407a20639105bf69005053bc0cd485b130e14d64c7d9d9b3104c11595e44a44213197deaf38379d4cccc033014e772cec4e65157e1431483c5088b090a78dd907e1dfc46230931c138ae6e9c34d87072dd2a27b8bfa71be947aa806aee2764d8599e6277ae85910bf38962dc95d39b5e39e4d2742b089bcfd4986b20f6793d442bbd9203f25e1ec244c808eb17d3263f352a9a7cee488f53910322ca4adbce24be4e347b097f66babe423b5182083a702572bcf99df48de47c5719b2c4625a42788250d40d02c85ba8096fe10758b50ca492ad7bc07f255df089349ccaf1b03b686ca7e21d409d9f7349625820bc2cac0d08aee67884c2a868cec6e802460b57940216fadd10b4a4bf0e786077ff3b712390f8cf59fa3ea77f892c058f45e04eac32061cf8c6ed309bf8dbc5cad324afa5a5801561c7b04fd61c2e3217774dae94f306781aaf4c97fedaeb13677f0c344183dce27d52d0df5d53acc6a2a184adf4a4663316c473261e687c3a23808de40e0616537e74f1946d2f32c7b134551f7c5fdc4717085bda5cc6a4bfa3ff0ad1fdbabf2564b63a440ff241ad8491842897b2accc08f14118fdb7b503a1003ec3b8b9e1188183a8c39b5b52b0e15a12ade1e094456893db4eb45d26df675a07ce38bbc3550d1c91139a1dc0d5d02b288ac444ed91c8903209bf63b044f07a4d219ca3908b5983362374885be143e33e75b86ca08541bd1a21bed824895eed95f9aca556a19b11a43ec39bf15621900ab3106007835faa9ce26ae702e5ba874e0aed0004ed753409cbe72b302c73077ff9d6b58a99acb03421d70f391dbf10efa0a1cbf7918b19655fefdf6cc1437228d4d704b02cb579bfd53514d19255dae882a982ac9d766f3f1f6ce0dc9f7f63c9ed00704f955301a255db995e1ddfa33170383516d4203bd11c8610da37f8ec7d5b7bc793911f4fb1ae1490925017a26be06097fecf4a425d1051321b8a1c642e7cb0ad8b5fb0ea766e2cdd5cf42cf23388ab60aa2f80e5c5b0220919956b605ee5c36407a95d9dd386c246d7f62d6c7933fdc05141ad75c8935eb68dd7566998c541faf20815918da84b3f7b1078c496bcfe8d4bf52a6357df887870cc2b8e32c6212b7c0f18c4434d9afbd95527f360faa220ce98adc4eefb0bce8e3898ccccdf49eb6aa8635628abf9e158f65f4d15955a348386e29cc55885211f56476f6c0e5f75fce33742f40e5fceb81a80786a4f902765b2f626d91ad6883572fa7c83f4c5843822e8cefc18e64867acb8a484ae89a36ffc27696a34a6cf78b659576d5a0a6d9a99862143c7f6838a728ae592f3d8259dab7fa831f4e26a669ab31b5e1b9c84231f6445cc19ed21931bfde9e242c13b3acfa5b2f652df7ecf0ce09ae6ca1e39b32f2f515c858c161da37c601d1ddfbb49b8727d228c182898dd21ab69ab47658aefb74fea442e0e9d53f609ca8ca7ad2a51fe179622e302c125e889bc508701b29938cd24819e4da5560dc659cf281a491549d9f3030072acc7e7c8ac41e58e1431da9f2adbc4caca3a7396b9cf18651812cd726f38f66d73764a831209f5e68a603d35921fa9640640d55ebc9c8907d13704fda26ebdaea66002ff5ae892366e195aed5b5e7fbbf1785b33b508c29af6ce8f3421890d8a466880cc803d6d936b49709844d25656e6be49d711e6a67798ce64c557225f9f471ff22d1a53fac8dc4e6a904bfce5c35c68f1e628bb390096523a909512fe3f103e50d27a0cdbafc2a1178b30df4521d444ad5fc232fcedfa0ba96a5f422b9d4341e8f1ab1413f6331f7af6a28fa58e46e6d410a1ad973ff20fcfaf5670a70b76c4fab65323d052ccdb90606e35a24fc0146968b7e5907b36d23cf6fce66b14c6344b5863de6c4701de6b774224e578f4d4137fc222593ef303a7d7be641063cfd0bd90b99e988c892895226a18039a3b141fd81b8d485d8e6ec9642610e8feabfa85085e002ea9afdff91133e129dd353b2022b97c51f6f38b3ddcb37b886cb3fab81e583dfc285174474e98c0da499f381b989a983007befa34090bde94e9488f4f2e85b1f2cda634599c1229f8d408dba2b6bba7732100796cc7ab21c43f578f7c701dbb33e0bcd6bd42b199c2eea7fc84fd2cfc56e5ef31a17c8f810bbc77c97043e46747be48576cb90ecce1d2e47633a7640132fdbfe12bc9e8391b39d5cf425e57b3a2b388c42ffbaf58ada2fa55fb2e6341416e0b733e08724e1f78dbd63928c4fc08de61a391539486d1f20ff16dc09a3f9f8fc83eb947fcea37eecc4589392bea7b0e31edcbfdc955b3290d420fa9b78fb1d58fdc213fa119ffffb09fe2b705882d51be4b3371e950e934456a6f71029fe98b3335f5dc74d3d8e839896c7e90aef787b5b29e326433a116069348ef5d6aee9f170914774dfdc8227d7ad1c12b7061819b5506110849922415ab3788673b4d9e0d73c18a765f6b40ff72f37ee2d8b5baa3d5efb03a9867c062ad5d2ed96394d91bc420736af1c1d7df3a666bdf1b2b5e45278575e3f03e2ce2662f44faefbed487bcdcf80a52721262886fc2984847387ec1c15692185f6e76e4bebdee77a0c1ef38e730b6df8dd483449f9b2863ebb74fe41d8444f8de2333772f3d27245df79504950622a5104386dcf63faecab88ad39873404b35d1468b0978a900c588f12a1876379b5d8a7ec519bf7239a6947528a9a5d99b82a2efd3a780cf326ecb6a8fee223280f5aa29d18f095719170805510c2f87aa11c2e8d1a95b7fdc11fdca421f5dfba1be9b73905051464500d841a4c61271e96ac0c3409a5ba3afc50cbb18612e1a11d9bede7d1b902952c4e9fafe430efa1508ca8bd7e74e9d183f613604d1bbf600be20871e6e27f78bd94bf0ceaa7ad63a33eee4a94542f03e36f07797847bde868735b5d39e776ef23b5d78611de6dbc4c306c2924de0789e4ccf6b5afbf8629b464cc8f0b5561d9acdd8bf73076081c98e08fd1d273a69b2765fc47d352757c7d9dcb2620c508bd9210a4fb9d2c9f161a75040d305c239f28a9691fca790ec33871cb229aaefec5c7243eb2d345390b77781c9bd6cd1c6c7e2ea57823d3a8cf9289d9c417c21a78e75b478a960d32b31f4dee2a93855f4de26525751c7d1e19018b440cb2caa5c466e3ef05c958369c4a0194d94d466386882895b0af2b214c005eaa5c98817e1b570118802e3d1c4dbd59de9e9282edb95ec63674670386016730638e3fe1dfd6ff71a117fd05a98300654cb2bd43a579befe7ec524ad57002773bcdb56ed57c634d91b8c33c1655bd24b36f1b1243659f67d552a63a93a6470012085f0e8e1ef54ba2e68f7c9654ea3fb0d803ed262b3eac94679441e8878194388e0a720be92fb3ec0e2cdae62c0583f98ee3d1350cc0e6f40fa2d30f9b4120ad7968b9f07e57a1f138f08e42068313b8d3f5eb8b81246ec564045fe13fcdb0a7192c1239eb61dad980edd66583e33a2b789d779a5579e2bba6ab42a52921b6ab3c8c13ebad78ee3f23cb2645a52c9b0d5c742b3133699efb1281de6088f421663150f10ec194009f16b364c6125e69c8a02ab637d6a5320ab29090afcd5d795cbb43a204b8ad60ce637ee786f1c2c0671d6c9cd95082b2a29f5e54d21e11bb6c09af84413e6d736c3ece2d04b8fa875558a34b352154287bf9b923919692b39d39a41f839f92c469966e6cc53d47771cedc92d533750ecbb1f80809dcc4e56fbae319a472ecd66f6807d1d4309f75d0d1c7049b1286366d7c4858d4f353a534eea63bb5e8a31e53c30f1fa2c18f8c20c7bdfba81e024ce198e2d097f537571b15a81985a436080a579cf47143e57092121e2610a3206fae438341572b2e963cfadd4dbcc367d7ba24a999c964dd0fb6b3576f001f6a59d57bc1871950c6f7219b09f46fba8d73f21bb52c0a1500fb766752ea55a114186ad52de2f6c2da16962f9d53dd7069fe114b8853c4eaf1709e28384fe874b8142c547b5036b250aa55390eca5a5d0e09719de9a74a7316c7676729501497856663741d7f12c13a52923b941911c5de797ce2dbc3db2c7781594fbcdca3f5484966a1d5b00033ec5f4f23af81f3e9d798c7fc12b4305457b32b7b0e5d269501758f75131a977d52105ab6b21e65238bcf49f4929bd05a53ca92b4a16236b877b2d635d53d064512a4f2bf5968014295461b4720cb0673a987ffba8ad142797d1d286b8aae6b02d9fa0ce78f5af888162a67820157f36bd4bd95a280c0f72dbb102e536d8bb5216dd731f2d845338b46fdefa513cc926cf924ad825cd716739e2e9146c96c974771bc92061615642d4ea809f2275e3ddb72f02499f342cfd057888ba604ae3a964fc4e770b6648af30bd6aa9e68904782a972c7adfa45a8f13a803d5187191d9f6cd835ed2b2bfbdbd8f36751fbbc0da9c8fac452ba548ff69313b8e25828432abebc67557398e4cde5a6bea1d7df78d3049a0cd949721df0774b2fb058c6571921d9d95a33b9dfa09d4a28966ff52146d251428e34a75171fbf0523d49b36ed6aacd446c689b457b4158733214d5bc733e7886bb52fa4fd289aba687ed56686db242d057fb157bfc3994a86f6deb86fbdd357f12f0c7925d242b1aa1c864b9d74bbfae9c42bbbabb639ea343cead5a16df9722ad16ffb7d76e08a64629a5521641c08b4fff2ae9c6c82a9e512f56adcae85e76ea15e7279553316663b389862990073f36f62f4687f2ebd19af4f2a0c22fb2cc3dcda3fd4c84d3270b4f924fae1eac5c19b1d72017d9d9aa10958805ccd5aedbfb2fc3940b92d12035bdcbcb779026605b0d3a64fb0374536a048273601ed9ebb4aca5f4af2825dc1b27236a026bf34f222f22daa2d6d1c4e4d7613a9bc44c4c1a0185e46bc0bcf831552dbc91786b3366d2dd744880c2ed988d62ca6268b0630c8994263d2a654823c4c2c08b149645cc073c5c597e34e03efb75641cfbc6651c6d68f1c05d28541c2c18d228c589e4f3d6075ba7ebca79dabbe9b1b45fc08a84319c71b2125a88fd422e8b6d891c275678ea881fbf3653aff60f54f4480da63e62778b631bcb2e73d2658c23229dd45086d895ce09e4b59d563e11a97561f62aac704a66fb3ab862d8984124983f6038d58a45ea2cb60ebd3bbdaad7e05672b2534af65fe10e065475965990cc7fe2daf191015905730ba0a4f4d0b27cd1851adf9131478b7ca0ff005292d75e3e1c3145c6cddde90a7a39cddb753004b508953ef28cda0fca22d3bdfb38cf6858e417f3fd212b4c4c423f1b508fcddb1d2d67a141cf37925c4ccbbdb95615cba3307b1a2f4546c4efed83a431cc881fba7ab8de4bfd6aca54fb817fc23b2b7205e473df97d8cc99c2df51608a4a4ddfba199752a9cb4037ddd30ac65bf663ef6dd45c3f9b2e1837f768a18f5390004f3bbef12d6e2850e4fe5b8b6b1dfeb77fd87b0b42c3982379d267c0b4ac493f69324cb7237bcf9910e481f1116b03e5d0cf2adb3e2a5f88a25ffa32c5f28250ec4ff43797f82aab2bcb74bd3adda39c036eb871d52b370c5e61b4be8a4c36c4f40dfeee2a2263359c03dbf2f664aa223b55b97a20d8b8b88ea5b84ec10b101717e4601177c73b7d8e1f9a06eb9468eea21a2800e0f1e4ae614accc8601c04a4f007e61acf99a4c3b7f672c0a9c4d670f4459531ebf85776ca128d01fef55348fc28de2861dfd08d85e1de9fc4f12884398edb3ebedd007502f9ef87e1081d439840782bc1e18e19f6e9e7b75b2786dcc39e4a1af3a944aa078ae48ccb642d0015d5b6c408e8532f938a4cd240f5e9e87c97af5a54d59e9aec20997317f578857b687bbb541e48b38a1a25f4f0f91ed5a5574c544ea8649df0a25d33dd3f44389087477c11e3b3ed3d8aa6bb15ca2137b88e08c6e06b63eb1fe035048eeedac3a95d9ccadefe43732acdb3d70eaf182f0763e6b6a95dbb92a32e81461563059938b16889bd44db9246b5452280e0de5ce8b37db34fcc58e7b8b26ada29870f96b3c6ba25d2e5b24e1897e18fbe7e8c949c48518779c667b1a59e8626b0a4685d4546d638595dfeb032e0baec5f90e055fd85a0f863deb691413aeb6f7f4fc721b8e12882fb18a130a9404cb330bd365794fadeafb350d62885ac8cbb4b17acd4476a90b76989bb0820b7106876b03def4037f5a24f6f7b3f2f3ca2c42e1d8485689c2080c8bf66e6c1eddb901caf3d952576244106b8d0d085b22af03c8a77719d1de149a7c189019f60e3565577bd98b898e710ec77a898af8d8654995c7af29134eec5589353e93344fefa8437449e8802573cb66b1ca8e46d696e6cb51121223bcb3b9b78a373fcaf88a9eeb306e52b5260f68ba4ade40ee9c76fbadf732cb9e62b1003c559a1aec22eeb113a42110f62749098d3c4dfea50f230eb985bcad6d5c363b057478a2857741f53c6ac7a3b6b15df0f6d594fe8e25ea4f67199564f6c926b8f2941c75828bc92a817d4524c719f6b2463ac6ad1fba63e0d2dba56db3ca90833137c9c3a8a3f632d985d43ead2224855af4f19e579bd3844eddc8a19734bcd56fb4c203d639ee1948b81a521ddcd688d5fe1bb47d48f74d20105ff7ef625b9a9b1a95546cc635382c9db26144912a01d41ea345986863167cf90a10e0a78e57ef657ac65f14b93f937275a4738cf69f468b4bf6834eaf67c4c77ead4ccba11848a08ce5d9d8b1a8247d8c9e225abfc9990bfb05f0046a923344a179d9d67ad22f400a84d2bb86c6f30a3e3fbb180ac9d836a22603dbedc61f5b0a422337032abddb7f0d42d0fedf6bc4326571050ff8ffb5844db98a2c0c419f10d5ddb1a9eb03517df521d897b370c4a3e19dd314ebc3873d2e810c93d47a9e1be154ef2827273922f2566e32f28f12361cea0f1c1808e0aed7b77fccdf229caada2b7edc1433a8ef8095c4c43e141badb7900f993be4adaf9654865bf5c3663698a3bd4eab81dda5295361dc3d736f2e4930b45aa2566df3185737b1faf25c7c76ae0a66f4d726f6c9ea43b639ee38a4898e9d69ac967c495234868ea5f34a3705dfa0b805071002ce6154e286ccaca0051f0fe398171e6a6cb167b1329560617aba8a3572dd28caf8c7594586daf9b9f9e3ba00c93c389b68a0b5d57716b8ae38e7131c1c2aceb6d3cb37a823d6c4935085dc0220eef272ec5213dc07be34429fb9c2c91cd5c46132acbad1b3d32355ad3d6e5b8cb9a592edc688220d3069d57398c49d8914e26799556d7874bc6603970a531ad34c5599a1a0662a3cafa6ca4a841937fbf8c675729608bb5c91c063a13a1a910683a07f538e9eaf0aefd76fe59136a4fe94fde1b4cce9c390435fa3c3d5e169f069be8b789551faa6af949ab5be68ecaf84ef23e5c1ea8613e61f7caa7e524791c3f0737b0728b803acb34816ee9e0555f557aa83cf898e9dccaee362174946365712208599a2d98262a2802b28549384501746e935134a99e8b253d1a3f13016a326bce4f94754a09501f750bae20f2cb52ca81885f2b2624da8978101319baf8efd67cafd55bbedd5b2898536671a4b8a6ccdb0411035c1cc4c2e0ddb1d329801ae216f7565b4e15a19b23369fef679681963a768b87239566232cb8619b43d49cab8ec43b062d91460daaecc4923ef65f618fb7945b476be781ef61f755847b8713547d40ed3997aeef164bbfddd223203db833e2b39a3bd63ffa5711986ce0c8129b018e6859785aa4a41f5738022b6c341c06706ddfd779fa9e0a9c0578c3e1460f4968844ceba247d166b1ba9c4a4f7ec4ebd8e8ad020147b2bbd7bfacd1da01772bb13c3c7865fab2623cbc9d346be8720a1ab8856139545967bf1b2e49dfcd2f19e43156085436f442a01d7697f3b353df680cc46a56e3af1b2cca7a1269b056200154335d06d2d872f58fff2c4ea37d4ce4aac033c0114da1370cf125c8c90b3c5e58edb13532243285aa576212441a5a1e9824509dc4ed41034ab7ecf65a4ec7279c28c1b9bb549342c41d3cd09bbe27c1eadb4a80404c05e54671cd5d71643daf6d75e0cf193183c42837967210f8638a4cfeb06e6539aa10a88747d08a4fd1518a8ef71efaf9a02dd3a3ea0958dafbe3a5b91a43790af9d576a356a4eac7bbe8062f7597c2480afd1a2977a95969e43636141c0f212176713dd7b7864157c215b77cbf8889341c5b5ac7293f7b4259f0b2e1c69091444e3e0c52ba1fdccd91acc79c086c12f89c291eeb0b96f20778bcea5eb7457bd95d1bcb526c84ed67ba9bfff903d556a31b8229ece59b32b97343e62b1670aba6d0c0416bc101ac4157503f6a0fed095d2e3532051f5008232be60f5562aaf6b5b214959a9ba057b4091968aa1257a721b0227abb012272dee4c187f076633a5f18cc920aca836e3e13b09890add2098270ae686f6cad9d101a6dcd7ecc3def808f0b144d891ee33bfe6f7e834a614b2925e05697ca7c15b5c1f507baff7f7ba941dfef8c78bf2c68c8b80bd4b679708b49253acf1e4ab842eca095502c4ac7a9c0ecda7bcb62219813d7fae024059c1865a2d370f636c02376eb58b53052186d4429255f11666759536e926ea8e9f4460f1e1e587e7c19da19fb7ee8725c38deaf843da5649a4808f8e091b09983f299c6ccb75dc3f6e1dfad0e9a06e598f742c0f86b6f51f0711383a2e24efb49959bb5247d44ba671c617ffc8227d9a3287889c0c582bbaae0c01b044beec0386c6df31f25d19beef780a44cdffa4906823f61c493aab0c9471cd02b55d7e8314b11565dbb027b863a12ac76a781cac460f5b81cf84b329d0f8e94b8212767c5dfd0749352085d4ad23f946d8629fcbbc2c3f400068339417f96d0a095fc609221dc3cc0dbf6329c15dbe78c989b10596b90bc32669b0f1b3de8cb7ef7845ced08ad0c65c08cc774ecee1d7b6f3d81d11d3acc1718d300a0b86b27ea0c2937517ad24c02e9feed0d87ee61abf3dd41b2991e4a8cd007f5d7a7c26c5432807b7f3a28b5ed9661e102c743023e673f4f2a80c1bf632980e8a8b9c50947b21ed63e20896cfcd71352e81b5f2c26f3019026d56315deddc12b3886cb39cff5184fd4237f765c998c802dbf3324d874d00dd0f0ba5b1a0a7a7d2686e0b252be7b5cec7aede148c15887495f99840bd3d96288ab4b73e581be1bf8a8b6f2ee18d96a8d9551e3d224dc7f2ff377ac06d29f29f8a8b9752c75b60297b30a0650d3224b42e628ced0191071ed4afeb7d6a886c076776216bfb97b46ee209b6bd622ba0503dcca0133e32f5d65224c50ce17cd5d465f44529c353f8dc7ad3ea36a7cae836d6ba25abb427834bc6441d275a30701e0536bea43f68cacf64840c6a74f70bde32364acc6dd9eb3a9c82203baf68e7d8bb05dd395751eb6fda7b884c038614c8eee4c7f09f2a1e97176e47bac8761b06f2502df051065b28a07c7ab5ff624632cd6da61abb59ef3548b57829b95ba83d33bdf857f1ae0dc27a2b1c43a34e9c58e11087856b20b348831ac6332cb6eb12ec760359825a00472a0bd1718338c81c114c34885aa861271591eb1353120c5a3e6881746d4b0a13e170aab2d2ca0016f49cf1c66806d86b7db0126cb9e2d0a903faec5b78d838b63241e32c6dda71936ba9ae1d162b7c1c9f0c5dc409f9373b9958a3343905283534e6fa1efe837a3278f7c1bfe147915bf877660a68977a4003d6c1a121ff52679ea10836040d3d71cf7c846c9289ea9785c2abf5b35374ae72c4a6717760d0e937a0fad6721fa0437280bf714c730391cbb5117ce60d7ccad567799a0a51e2174a22fd8deb1522c5661f7f61121b40e2b98f01d1f2aeac6b2cdea1424e82e4784b00ed7597122723cf2b8affd02f5013e4d841fa6aa6681e686c846ef439db3530322102676b59588c37233dc8552ad2676cf96e0216addb972835df6d3086783362baf2818eeb667cd49e17a6411a0754454f8433dbd953c42bf997da48c74e3280c74f0bb10f1d9a7634792c9d5b29870179d7d0f2c5c4f96efac72a8dce316531cf4380195e62cc788bd3f42a67946cb66351de88900256350583c4eddbf7b41e59f4d1579ab306f32e8f973b86a6cd1dce7d363dfceb027f57ed281129d6e2a6ffb7da587390205f5c815f81c81805d7c3ff71895081db4d9ec79622df4f7ab4382bc3690836681511183645c169300b2cc9decafac29a386a2d4288191c5808a20f75d20ab0fae0c0d5268e57549078d99f0cde69acd5b7623b2010b4c56124e5469c108d3bf27a58a813ac101754f4ea42610f4691b80863dbc909283e13e32f37943b34fd34a5162b86a12b6d26d1819a21ca1e63f39cc981747f706adc4e7f5fe8518f6d774b04da70eba8b86fb4fb4a35a536815876f36df9682f25f12ee5eb0641bb6d6d4c0c98d3778ab655531a1b9550d8f6c80dbd127f0833c218e1668f40e4bb469356148df33f366c97be8beab2381f3d2dde0ad3553cba6874423d59ff06c78e65053980f9e2c757ce9a3dfa76b5d0e4bad33f36076ab8df1d7403ddece833d8c16635fae350cab59fd1f6a377ac44b83ce28753da0aa553bd196339b0abea77e07db5e73c9fe15a3c1dfd67715875728c72ffb37b92902ec71086c82fa88a887161af590f5e05cc3290052ea1eff9bc79c5f0b5c8c695feb7f43e9295a724cf514f9e0bb87e2d9abe4147a1b4e6e88169cfa59b0710a2c376439056a6a473187c7e63cd8c70522c9e1b874b93c8f92753020446856282b05d0eb5c520bf08fe569e19e53797bac916eb5ebc00ddda6c5a9e0e96cab1fd2a173ded52d203e654149f0cc4c93436b493583f8e07688a6e49ea74b2b858081b6ea7024402868e94288cb1450469d0ab49d30d6db886a211d39266611cd31b2ed84d5111a0a181c0b316f84d178d89a7b6d87f4c4bb83d56b081078ee40098e2971f6e4a3e56df35bc588b5538e541c76ef2e6fb037b7bf274b87d8404b85a4d6ffd6e0e6318de21020b36f44d969f1366fc0d067a642186f74c024dbe6f2ff6e200e6100db4e3ea392b258255d01f6fe7bd7d0a370d9adeb23fdbfbbabe6a06b7fa47d8f4acda445a7cf88e28c974fcae7059a7a737ac44607883eba2230326598020d692ac59582889da930cb2cad8326b0d56d62530dd3523d30d50b998f4809afb25b3de4f54ccfa8772a313b375c9542f2bcf1f9996e02ebb292feccb4f87fe8ff6faa689def150deacdc9dd598f2fc0b61737b124eb51cee7861b5a4bd0b8f68319b962405f0d45ef1669b9c1005a887e6cd516f9915a0c9715a3d2001c14c4c72eab45b8609b66833579a0718b56d5a45b372bb283c08dc6fe7361884a027b3ce6854302b4c8bb89d60220a53aa6482645882cc2433ad4589c6b41d1224389551ba241cd63ee96f3b6fc2302f1eac5f5d1f44e914ed7f66d484da13d952da0bcca21d8806f533708659f028c43137fe19d1810c322020fe691c11bfdde452a1d33d7c76b230e989a788a61d7e041e8e92f4e7431a36c7ec0418f2f204e090b5de7b52cb4612178099d4c1d2cab4c681b6e9c871453129bf65828b90a64b9345bfdab9b00dbaa2b62d88bae9c7921e681dde16bbf98d9fa6d66adbb67962dd8a61e511158ae5e77f80de10b2afe9cb970b1eac8328a4041ff285be3d512299dd2c0eb4955871cc2f6f43cb48a47f4d46199656f104c6834ed4d18fc74a520d1900ae9cd4236170c61b39b507f087c3793c7d85f9b81d3edec2b9c11fe9d08fb1b33b6588c6744f9da43a6203adab0d8d560c241c34e714093c2f889689d9f8b3c236dfc9e944cf6649b3c483900b8c86e14512c89213af91207fb87d5718478c7bce0fee2e5208ba923ce3e37f955f7baf6470a355bddc6eb2e7272cf84af3d849f29b546ba8e29d3f4b9f72a8eb6536787038e44f62e0bdf8e246fdcb1ad49efd51724545b0268795674d9b3b4ea89f3d402cf93bfd134150233d1079523f46f9c2097f621f419c223f92d3cba36e742462603936784988bcac1bc64b32083f72bd5db0654c401f539099c4deb5f914631fa72059fab5ee2014c416b85f358c73af7413aab2937f3db48cf562e9dbde422cf7741f94e633add8ee6b82e05cd9af2ca2aa255bc39fbc4dc83580ee5daecad21ea3ead3f73ba80947b613019d2d4ddd01b8a94876eaf9bd4723de34f83cb3a57a6105bf7bbfda7d6a71339639b2daf2c8e0332a236811a767c031e527e16a6594ebee3dfb55934a9abdaa58bfab632977af6485827f404494bedce2c1c1f0f0660155ba42c6ba43914eb2e7727334bfc11e2c482971dab8da0015e2bf9e4d0e9cb2e1f11ae5998e3d74fd6ec85734cad9d7a2f5dac7acd11bc8d051b7730d6badc818584ddfe8dc4e27c7d52a0fec4708f7a27201b78674e8b6cb0a5aa3c2a61ba770f3c10ddc3870aca7ea6a99200949417ad5ae089045987f852cd207574bc6bc5a3b9116e26707c546bf3b617e86bca173333af5492bf8c3614ed4a4006b8bcdf0e42a86b532685a9a2a77f4dd84d8ea0c69284955ba839f685b30aa24590c13cb9363ff31f898629065d71afd1bbbbab16c1929f820cfc61c1b00bcc918fe0fc2f331e6a18a31d21920ac6e01e5b773f0c41d11a57273f31611c08a13ea3966f579aa1ae85f2e452c21cc70f5c9de8a81459a3608ca1db241bb993cdfabd81bf47218565f3c3a8a34b0191638a5e7d772f1da61f5c625900cc4bfb998fb72d7495242b16017e94fcfee83e302851ea1b7d56f434ba51e0c5c9c3015e0e8caceecc06f9fca852be9c110ca0e5a60b1e098de67cc4908859e5d309b4ea6860971c3b7b90c4a478e7bd61f9d9d79fdba34beff044e14f40c523ed6dcef3a1fb8c8d03f9cecc0f1c12bc061a03fb20016903f974f2f889114a138430327dcb4c9312ff8b71743718bebb1f376d56cde711d2eae48547d11b79476c7d1473b0417b4aff9ba18246608c1fa97f725d0836d4d84cbb044dccb5f21a9713aee3a4629079dd3c87ae599c7af38e1d159f36f4c704383c4074c49dd92c3d33fc3c7b3b29aff3e4af12ebf6c0bb1be89583985bd60ae691d28f17dc16cf834ccfce56af5498bb01e1b0697b5abc94ba6ed8384ec006611cec30c0a1698b8177c0671444b2e5278115ff51355d3ec9ef25413923afff6a49d2e70312037059e8ea25e2bb2f52618b74225258d5215687c76ed2279c351f6398d625efbbf7e9b81e72c6c683aa7d6be625abb8a75a53a44a88ded3c237fab48bb0d4c699a66a5353504b53bb2ce2a768585cb35093464fa151ea12a968ea6f6988eca954d92b47c49f740c6c580d3ee564d6c8dcb98eae1c0ce56f113c50a986c8a18189654eb6fe7c0b0bf90d6f5c6008c0352dcdbbbb524c03568213555e493f270b560924fe296f017bf2e572843d76dbb3b181548010d2c77e3a70579fe1ef88a5a494c14e5d7db1296a2a6fc6a424a2525570b592746190cf3d407bdfb04eca41df6c5fce63b871fdc4115cb56f6e9757b4c50899439264439836ccea4a73b29456be5d069fe6a9001301b66810f88ca66444581acdaa543ad6d05d143bdd4e503bd241cd6cf762a81ee9f61ed488bfb3a5b4b7a3d5aaf6d22af5fefc6b02f5dda2e52d11cec7c16e02939ada4c04a432345658c3cf39d3281fd9e5ed5ec5789de6e4cfecc3fb698599cc1a29ab1f9d8508126dbdda27d263d8b933a0bc1f376a637070fce8369dcf63f3a654797da7742710e34fb36ae239f18471091381891d9a64ba6e2baa675adfb8fb235de5a0d7075833fb60e3f13a7c31a85720a6722a7f7b1147e0a17438a08a127d9babb46a19a53b6d609250a00106842ff6c2b27fd25e82009e6fd069262795","plaintext":"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fa000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacb"}],"peerKey":"ac01b2209e86354fb853237b5de0f4fab13c7fcbf433a61c019369617fecf10b","second":"f5b2d6e60f9477e310c2982daaa6c9136c108a1777c5947e448fa37d68174557427a4df2d6a1c8c54eb7460e8f0d4fe3b281931b289d5cd67b81355efa4a6cbe165b245eb658a27a53aeb79036c2476376bba0d67de666547389d4a3329086f72b0fea3b5e0100181ca93afd35fece4b","third":"5b602e19479c2504ba2f7eed39399d9ecadc4e49aaed07d693a67df674e18ccbf226a042cd9eae46abec029a26ef7e01a529b259a88e108fcd430c4dfb5f0821c7c552bfae2bcce3bcefa8545cec88b9"} diff --git a/crates/crypto/tests/fixtures/primitives.json b/crates/crypto/tests/fixtures/primitives.json new file mode 100644 index 000000000..2c82e458b --- /dev/null +++ b/crates/crypto/tests/fixtures/primitives.json @@ -0,0 +1,200 @@ +{ + "version": 1, + "purpose": "Public test keys only. Primitive compatibility, not the Zeron wire protocol.", + "aes256gcm": [ + { + "name": "gcm-aes256-empty", + "source": "GCM specification test case 13", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "000000000000000000000000", + "aad": "", + "plaintext": "", + "ciphertext": "", + "tag": "530f8afbc74536b9a963b4f1c4cb738b" + }, + { + "name": "gcm-aes256-one-block", + "source": "GCM specification test case 14", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "000000000000000000000000", + "aad": "", + "plaintext": "00000000000000000000000000000000", + "ciphertext": "cea7403d4d606b6e074ec5d3baf39d18", + "tag": "d0d1c8a799996bf0265b98b5d48ab919" + }, + { + "name": "synthetic-binary-with-aad", + "source": "CryptoKit-generated public test data; independently checked by ring", + "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "nonce": "202122232425262728292a2b", + "aad": "73796e7468657469632d616164", + "plaintext": "00ff7f800a0d7368617265642066697874757265", + "ciphertext": "d2c5d9f0669569667b0e27aae17e9d81a43c9ef9", + "tag": "dd70040212ec1131d455cbef70eff563" + } + ], + "ed25519": [ + { + "name": "rfc8032-test-1", + "source": "https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "message": "", + "signature": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + { + "name": "rfc8032-test-2", + "source": "https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "publicKey": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", + "message": "72", + "signature": "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00" + } + ], + "hkdfSha256": [ + { + "name": "rfc5869-test-1", + "source": "https://www.rfc-editor.org/rfc/rfc5869.txt", + "ikm": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "salt": "000102030405060708090a0b0c", + "info": "f0f1f2f3f4f5f6f7f8f9", + "output": "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" + } + ], + "signedRecords": [ + { + "name": "draft-wrapper-v1-content", + "source": "RFC 0001 section 7.5 candidate wrapper; not a production protocol", + "kind": 3, + "vaultId": "01010101010101010101010101010101", + "generation": "02020202020202020202020202020202", + "epoch": 24, + "objectId": "03030303030303030303030303030303", + "authorId": "04040404040404040404040404040404", + "revisionId": "05050505050505050505050505050505", + "membershipHash": "0606060606060606060606060606060606060606060606060606060606060606", + "payload": "00ff1020", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "signingBytes": "7a65726f6e2f7369676e65642d7265636f72642f763100aa000101030250010101010101010101010101010101010350020202020202020202020202020202020418180550030303030303030303030303030303030650040404040404040404040404040404040750050505050505050505050505050505050858200606060606060606060606060606060606060606060606060606060606060606094400ff1020", + "signature": "7b01f902b880d79bcf51346e81121d6c207793d973d6e22f39a7b5de25bd3eb9a0d2d5e115754e528acb4c0f25865f41a1d6c84fda70b43f7636f1e1b1931a02" + } + ], + "recordMutations": [ + { "name": "nonminimal map length", "offset": 0, "remove": 1, "insert": "b80b", "error": "NonCanonical" }, + { "name": "indefinite map", "offset": 0, "remove": 1, "insert": "bf", "error": "Malformed" }, + { "name": "missing field", "offset": 0, "remove": 1, "insert": "aa", "error": "Malformed" }, + { "name": "unknown field", "offset": 0, "remove": 1, "insert": "ac", "error": "Malformed" }, + { "name": "tagged map", "offset": 0, "remove": 0, "insert": "c0", "error": "Malformed" }, + { "name": "reordered key", "offset": 1, "remove": 1, "insert": "01", "error": "Malformed" }, + { "name": "nonminimal version", "offset": 2, "remove": 1, "insert": "1801", "error": "NonCanonical" }, + { "name": "unknown version", "offset": 2, "remove": 1, "insert": "02", "error": "UnsupportedVersion" }, + { "name": "negative version", "offset": 2, "remove": 1, "insert": "20", "error": "Malformed" }, + { "name": "float version", "offset": 2, "remove": 1, "insert": "fa3f800000", "error": "Malformed" }, + { "name": "duplicate key", "offset": 3, "remove": 1, "insert": "00", "error": "Malformed" }, + { "name": "unknown kind", "offset": 4, "remove": 1, "insert": "04", "error": "UnsupportedKind" }, + { "name": "nonminimal byte string", "offset": 6, "remove": 1, "insert": "5810", "error": "NonCanonical" }, + { "name": "text identifier", "offset": 6, "remove": 1, "insert": "70", "error": "Malformed" }, + { "name": "indefinite byte string", "offset": 6, "remove": 1, "insert": "5f", "error": "Malformed" }, + { "name": "identifier length bomb", "offset": 6, "remove": 1, "insert": "5bffffffffffffffff", "error": "SizeLimitExceeded" }, + { "name": "payload length bomb", "offset": 134, "remove": 1, "insert": "5bffffffffffffffff", "error": "SizeLimitExceeded" }, + { "name": "zero epoch", "offset": 42, "remove": 2, "insert": "00", "error": "InvalidEpoch" }, + { "name": "nonminimal epoch", "offset": 42, "remove": 2, "insert": "190018", "error": "NonCanonical" }, + { "name": "short signature", "offset": 140, "remove": 2, "insert": "583f", "error": "Malformed" }, + { "name": "short identifier", "offset": 6, "remove": 1, "insert": "4f", "error": "Malformed" } + ], + "ed25519PointEncodings": [ + { "name": "order-four zero", "encoding": "0000000000000000000000000000000000000000000000000000000000000000", "allowed": false }, + { "name": "identity", "encoding": "0100000000000000000000000000000000000000000000000000000000000000", "allowed": false }, + { "name": "order-eight y1", "encoding": "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", "allowed": false }, + { "name": "order-eight y2", "encoding": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a", "allowed": false }, + { "name": "order-two minus-one", "encoding": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "allowed": false }, + { "name": "noncanonical p", "encoding": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "allowed": false }, + { "name": "noncanonical p+1", "encoding": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "allowed": false }, + { "name": "noncanonical maximum y", "encoding": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "allowed": false }, + { "name": "canonical p-2 encoding only", "encoding": "ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", "allowed": true }, + { "name": "RFC 8032 public-key encoding", "encoding": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "allowed": true }, + { "name": "canonical two encoding only", "encoding": "0200000000000000000000000000000000000000000000000000000000000000", "allowed": true }, + { "name": "short point", "encoding": "00000000000000000000000000000000000000000000000000000000000000", "allowed": false }, + { "name": "long point", "encoding": "000000000000000000000000000000000000000000000000000000000000000000", "allowed": false } + ], + "ed25519ScalarEncodings": [ + { "name": "zero scalar", "encoding": "0000000000000000000000000000000000000000000000000000000000000000", "allowed": true }, + { "name": "one scalar", "encoding": "0100000000000000000000000000000000000000000000000000000000000000", "allowed": true }, + { "name": "L-1", "encoding": "ecd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010", "allowed": true }, + { "name": "L", "encoding": "edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010", "allowed": false }, + { "name": "L+1", "encoding": "eed3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010", "allowed": false }, + { "name": "high scalar bit", "encoding": "0000000000000000000000000000000000000000000000000000000000000080", "allowed": false }, + { "name": "maximum scalar", "encoding": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "allowed": false }, + { "name": "short scalar", "encoding": "00000000000000000000000000000000000000000000000000000000000000", "allowed": false }, + { "name": "long scalar", "encoding": "000000000000000000000000000000000000000000000000000000000000000000", "allowed": false } + ], + "ed25519Rejections": [ + { + "name": "identity-key signature", + "publicKey": "0100000000000000000000000000000000000000000000000000000000000000", + "message": "", + "signature": "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "negative-zero identity key", + "publicKey": "0100000000000000000000000000000000000000000000000000000000000080", + "message": "", + "signature": "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "noncanonical identity key", + "publicKey": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "message": "", + "signature": "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "noncanonical identity R", + "publicKey": "0100000000000000000000000000000000000000000000000000000000000000", + "message": "", + "signature": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "noncanonical S plus L", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "message": "", + "signature": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901554c8c7872aa064e049dbb3013fbf29380d25bf5f0595bbe24655141438e7a101b" + }, + { + "name": "scalar exactly L", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "message": "", + "signature": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" + }, + { + "name": "small-order R with regular key", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "message": "", + "signature": "01000000000000000000000000000000000000000000000000000000000000005fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + { + "name": "noncanonical R with regular key", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "message": "", + "signature": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + } + ], + "encryptedContent": [ + { + "name": "encrypted-content-v1", + "vaultId": "01010101010101010101010101010101", + "generation": "02020202020202020202020202020202", + "epoch": 1, + "objectId": "03030303030303030303030303030303", + "authorId": "04040404040404040404040404040404", + "membershipHash": "0505050505050505050505050505050505050505050505050505050505050505", + "purpose": 1, + "keyId": "06060606060606060606060606060606", + "contentKey": "0707070707070707070707070707070707070707070707070707070707070707", + "signerSeed": "0808080808080808080808080808080808080808080808080808080808080808", + "publicKey": "1398f62c6d1a457c51ba6a4b5f3dbd2f69fca93216218dc8997e416bd17d93ca", + "plaintext": "70726976617465207472616e73637269707400ff", + "encoded": "ab0001010302500101010101010101010101010101010103500202020202020202020202020202020204010550030303030303030303030303030303030650040404040404040404040404040404040750000102030405060708090a0b0c0d0e0f0858200505050505050505050505050505050505050505050505050505050505050505095863a6000101010201035006060606060606060606060606060606045820101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f05582459810eb900ed20065ddcf078cdc6058678314d7c772b113f7a6fa5ca4bedeaad37fc67600a5840ccf11a4602d378560442555ab3ee8a6c517c2dd41a1c2cef190cf417a6f90342c32e637f2b5b76b39bf8baff4b97f9d104d878cee25b09bd3974c99f6a93f60a" + } + ] +} diff --git a/crates/crypto/tests/fixtures/vault.json b/crates/crypto/tests/fixtures/vault.json new file mode 100644 index 000000000..87b4fee2a --- /dev/null +++ b/crates/crypto/tests/fixtures/vault.json @@ -0,0 +1,59 @@ +{ + "vaultId": "11111111111111111111111111111111", + "generation": "22222222222222222222222222222222", + "orgId": "org_fixture", + "userId": "user_fixture", + "profileHash": "5f249d8b09b39137298ef222a50f428faddbe0c608ce793c5162cd9eee491153", + "recoverySecret": "4444444444444444444444444444444444444444444444444444444444444444", + "recoveryKit": "IRCEI-RCEIR-CEIRC-EIRCE-IRCEI-RCEIR-CEIRC-EIRCE-IRCEI-RCEIR-CGLPY", + "recoverySigningKey": "90fc254f2d2ac531859cc9711c077eb311534ccb6778ad29c99e8034b02ec146", + "recoveryEncryptionKey": "a6cd8a5bfdb8b6fc0cee06e2a64efff5415e66644cf7ece1e6dfc55a216f425a", + "recoveryAuthorityId": "71754fc11f63a3855c569772b7237070", + "deviceA": { + "id": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "signingSeed": "fba1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "signingKey": "7fa44d7552061d18bb8120cebaa7caf8d3c5924c748053f55b1270fa5d3961b0", + "encryptionSecret": "9292929292929292929292929292929292929292929292929292929292929292", + "encryptionKey": "79fcf7f4c04d3277b425449f0d32209342908b4bc3772dfabe315f82034f6e64" + }, + "deviceB": { + "id": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "signingSeed": "e8b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "signingKey": "73442a62ac03a0c75b2b10d32fd8b5a0c73bd764a06b0ed58aface6b9feac019", + "encryptionSecret": "8181818181818181818181818181818181818181818181818181818181818181", + "encryptionKey": "00f81ab0eb0e18ddb5e247d38f25c7a352c39fc77a74cb3f739d309f69ee4878" + }, + "membership": [ + "qwABAQECUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAQVQAAAAAAAAAAAAAAAAAAAAAAZQoaGhoaGhoaGhoaGhoaGhoQdQrhYfXwsd1s9d/FnciYFoEghYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACVjuqQABAQACWCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANYIF8knYsJs5E3KY7yIqUPQo+t2+DGCM55PFFizZ7uSRFTBAEFAQZYIJD8JU8tKsUxhZzJcRwHfrMRU0zLZ3itKcmegDSwLsFGB1ggps2KW/24tvwM7gbipk7/9UFeZmRM9+zh5t/FWiFvQloIgYRQoaGhoaGhoaGhoaGhoaGhoVggf6RNdVIGHRi7gSDOuqfK+NPFkkx0gFP1WxJw+l05YbBYIHn89/TATTJ3tCVEnw0yIJNCkItLw3ct+r4xX4IDT25kAApYQKdMczFb4QV6ZEpkQ1qBj8vpFrwrrbFGqSu+ZaSMjEQdhQOuLmSU4ORJ5TKEMdQNEruLsdDBm0kKS97BvFNm9wA=", + "qwABAQECUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAQVQAAAAAAAAAAAAAAAAAAAAAAZQoaGhoaGhoaGhoaGhoaGhoQdQTjy8lUwBVREF2zcceEGK8whYIEmlEWBPmLH+hmbPVU4kE2qgSFjDm8Vjh2YqYOaBpn1pCVkBRakAAQEBAlggSaURYE+Ysf6GZs9VTiQTaqBIWMObxWOHZipg5oGmfWkDWCBfJJ2LCbORNymO8iKlD0KPrdvgxgjOeTxRYs2e7kkRUwQBBQIGWCCQ/CVPLSrFMYWcyXEcB36zEVNMy2d4rSnJnoA0sC7BRgdYIKbNilv9uLb8DO4G4qZO//VBXmZkTPfs4ebfxVohb0JaCIKEUKGhoaGhoaGhoaGhoaGhoaFYIH+kTXVSBh0Yu4EgzrqnyvjTxZJMdIBT9VsScPpdOWGwWCB5/Pf0wE0yd7QlRJ8NMiCTQpCLS8N3Lfq+MV+CA09uZACEULKysrKysrKysrKysrKysrJYIHNEKmKsA6DHWysQ0y/YtaDHO9dkoGsO1Yr6zmuf6sAZWCAA+Bqw6w4Y3bXiR9OPJcejUsOfx3p0yz9znTCfae5IeAAKWEA3XlwJEdRnF52iHuSPzS0GHIzLuulFGMRFX2Sh2qUoY0mxhBprpOTQHtAZ65P58uT0t7EwQFV5mBOCNFTwA0sM", + "qwABAQECUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAgVQAAAAAAAAAAAAAAAAAAAAAAZQoaGhoaGhoaGhoaGhoaGhoQdQlJjVZzPbpdZrAh6vuYiYIghYIHf/pe4B4HcJcu2xEimH+7U3qakSApiF6tnivcB4eSvQCVkBRakAAQECAlggd/+l7gHgdwly7bESKYf7tTepqRICmIXq2eK9wHh5K9ADWCBfJJ2LCbORNymO8iKlD0KPrdvgxgjOeTxRYs2e7kkRUwQCBQMGWCCQ/CVPLSrFMYWcyXEcB36zEVNMy2d4rSnJnoA0sC7BRgdYIKbNilv9uLb8DO4G4qZO//VBXmZkTPfs4ebfxVohb0JaCIKEUKGhoaGhoaGhoaGhoaGhoaFYIH+kTXVSBh0Yu4EgzrqnyvjTxZJMdIBT9VsScPpdOWGwWCB5/Pf0wE0yd7QlRJ8NMiCTQpCLS8N3Lfq+MV+CA09uZACEULKysrKysrKysrKysrKysrJYIHNEKmKsA6DHWysQ0y/YtaDHO9dkoGsO1Yr6zmuf6sAZWCAA+Bqw6w4Y3bXiR9OPJcejUsOfx3p0yz9znTCfae5IeAEKWEC5JFdCPWh1mLgoDsVSbEJjRR6vL1x4q8VJAaHTstNd89G22vq31aW4WAf6I0VGGBR3ZC7sNLgk1D7dZIMNQ3sG" + ], + "membershipHashes": [ + "49a511604f98b1fe8666cf554e24136aa04858c39bc56387662a60e681a67d69", + "77ffa5ee01e0770972edb1122987fbb537a9a912029885ead9e2bdc078792bd0", + "2eb91aec29aaee8784a2991859917b811d99ea37e1a5d14455bdada553b3ee2b" + ], + "epochsAfter": [ + 1, + 1, + 2 + ], + "keyringEnvelopeB": "qwABAQICUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAQVQAAAAAAAAAAAAAAAAAAAAAAZQoaGhoaGhoaGhoaGhoaGhoQdQlnd5FC66UWkVa2tqicfvawhYIHf/pe4B4HcJcu2xEimH+7U3qakSApiF6tnivcB4eSvQCVh2pQABAQECULKysrKysrKysrKysrKysrIDWCA1wcVFaptV5atLEOMNLKWK1lb/hyWhfz7wHXL0gC8VPARYOTQiKpRNl6ckLuDjVcopk0qzhuY3cD3Z/il2yfTAl9MVLw2xXE763Mrlh14ROmTBNtI/1wpFlQ61uQpYQBnRZXGRKBoKDZOGHVY8JJDRTJdi+tU+lQJi/B9ohqs9xcJVi31Tb86noOOSwSN1GPeJNyS86Vlzl/awZ8W5nQY=", + "keyringEpoch1": "6666666666666666666666666666666666666666666666666666666666666666", + "objectId": "77777777777777777777777777777777", + "objectKeyEnvelope": "qwABAQICUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAQVQd3d3d3d3d3d3d3d3d3d3dwZQoaGhoaGhoaGhoaGhoaGhoQdQsVLcPWw2MJ9J1CKVH0GNFwhYIHf/pe4B4HcJcu2xEimH+7U3qakSApiF6tnivcB4eSvQCVh9pQABAQMCUAAAAAAAAAAAAAAAAAAAAAEDWCDlemjcTXYc2VlRdy7wAh1zc1Zs3Y0kfTHUfv2Y+7inZwRYQK8D8XdiMQXA4HxBEIDYE7ZV9QETwAcmZUzQMdHvXqmylOoWm7HivKXi2PV4w4JCoIbRbbDmTIbrGCiMyljNLhkKWEBDOfat4BzLvXzBg/KQn2JBj8/eP/bcuOZVbcXlFdDXGy97Pn7pUDrspIHZHmu9bptXa+YvI2cbQScFL7XvTeIJ", + "objectKeyId": "88888888888888888888888888888888", + "objectKey": "9999999999999999999999999999999999999999999999999999999999999999", + "chatRecord": "qwABAQMCUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAQVQd3d3d3d3d3d3d3d3d3d3dwZQoaGhoaGhoaGhoaGhoaGhoQdQTBXRc+EjVVg7c/UMQCEEqQhYIHf/pe4B4HcJcu2xEimH+7U3qakSApiF6tnivcB4eSvQCVh3pgABAQECAQNQiIiIiIiIiIiIiIiIiIiIiARYICLSfuTzr7Zqh0eAEx8MSQOKanstS8PZjEc3WcV2RaBxBVg4hq9yV2PZXtCJ+bWX7l6hYW/r80hrQP4+CsblUAOSkaE4BU8HRQUH6XQDvqWyMNtsO2ws7mrb1B8KWEBUvCJqCrWjjvBbE7sBgaJvyrjyFg8/CBKIxG4m9Ul+HjTsrt+3akL9fK24N82tv6cm4W3jj8BI7YxZOLT/iUMI", + "chatPlaintext": "fixture chat update: private canary text", + "registryLifecycleRecord": "qwABAQMCUBEREREREREREREREREREREDUCIiIiIiIiIiIiIiIiIiIiIEAQVQd3d3d3d3d3d3d3d3d3d3dwZQoaGhoaGhoaGhoaGhoaGhoQdQTfP24+lone9EtlaBv8tIxwhYIHf/pe4B4HcJcu2xEimH+7U3qakSApiF6tnivcB4eSvQCViepgABAQECCQNQiIiIiIiIiIiIiIiIiIiIiARYIBqJY3XhTTtOytLGVa/5PjnrgiqAxRnrHMqfGQXPTKD0BVhfXfvjRX71I80ORfJDZGiVVvN5krLUAx7pmbUwmOYhbfClCvAEEReW5JkZFatdDhbm3BGeynWliDVZ6PCzs3an4JHqWJESJ9JKVWzIc6E0JZji7qvr23ZZ2koZuFbAnvoKWEC+9HJOlJJMgt1ZdFALEDRiZY58kGfYaESJZCCr/H042Xc2CmE8hJzSX/7mr1l5VuUmnkMEM7nNarsHioOMCucI", + "registryLifecyclePlaintext": "{\"kind\":\"chats\",\"id\":\"chat-1\",\"op\":\"delete\",\"hlc\":\"0000000000002-000000-dev-a\"}", + "enrollment": { + "requestId": "55555555555555555555555555555555", + "deviceId": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "signingKey": "75a4595b96a62e712c8f6b2934792af1bd38229cce55e22f27dc734bf7020415", + "encryptionKey": "b40c34835815ab31869f8f7b009199f7b10c45157794d57b37d5716862fad115", + "proof": "495f7f48eaf1969f8eabcdc06745ebf4c65ff8fdd7071634a40070acdc4b5e0edc2f97c201082624002f41ba339d2cdf55585e1f67d71e117001c33597b56c0d", + "pairingCode": "3955-6166" + } +} diff --git a/crates/crypto/tests/vault_fixture.rs b/crates/crypto/tests/vault_fixture.rs new file mode 100644 index 000000000..6df6c9e1b --- /dev/null +++ b/crates/crypto/tests/vault_fixture.rs @@ -0,0 +1,441 @@ +//! Shared vault control-plane fixture (`tests/fixtures/vault.json`): a +//! synthetic genesis → add device → revoke device history, keyring and +//! object-key envelopes, an enrollment proof, and one sealed chat record — +//! all under throwaway test keys. The edge (TypeScript) and native (Swift) +//! verifiers consume the same file, so every side authenticates identical +//! bytes. Regenerate with `ZERON_WRITE_VAULT_FIXTURE=1 cargo test -p +//! zeron-crypto --test vault_fixture`; the default run verifies the committed +//! file end to end. + +use serde::{Deserialize, Serialize}; +use zeron_crypto::content::{self, ContentKey, ContentPurpose, DeviceSigner, KeyScope}; +use zeron_crypto::envelope::{self, RecipientKind}; +use zeron_crypto::hpke::HpkePrivateKey; +use zeron_crypto::keyring::Keyring; +use zeron_crypto::policy::{ + self, DeviceEntry, DeviceStatus, EnrollmentRequest, MembershipState, Operation, + POLICY_OBJECT_ID, PolicyPayload, +}; +use zeron_crypto::recovery::RecoverySecret; + +const FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/vault.json"); + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DeviceFixture { + id: String, + signing_seed: String, + signing_key: String, + encryption_secret: String, + encryption_key: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Fixture { + vault_id: String, + generation: String, + org_id: String, + user_id: String, + profile_hash: String, + recovery_secret: String, + recovery_kit: String, + recovery_signing_key: String, + recovery_encryption_key: String, + recovery_authority_id: String, + device_a: DeviceFixture, + device_b: DeviceFixture, + /// Signed policy records in order (base64) and the membership hash after each. + membership: Vec, + membership_hashes: Vec, + epochs_after: Vec, + /// Keyring envelope to device B under the head after the add (epoch 1). + keyring_envelope_b: String, + keyring_epoch_1: String, + object_id: String, + object_key_envelope: String, + object_key_id: String, + object_key: String, + chat_record: String, + chat_plaintext: String, + /// A registry row lifecycle proof (purpose 9) for a delete of + /// chats/chat-1 at the clock inside `registry_lifecycle_plaintext`. + registry_lifecycle_record: String, + registry_lifecycle_plaintext: String, + enrollment: EnrollmentFixture, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct EnrollmentFixture { + request_id: String, + device_id: String, + signing_key: String, + encryption_key: String, + proof: String, + pairing_code: String, +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn unhex(text: &str) -> [u8; N] { + let mut out = [0; N]; + for (index, slot) in out.iter_mut().enumerate() { + *slot = u8::from_str_radix(&text[index * 2..index * 2 + 2], 16).unwrap(); + } + out +} + +fn b64(bytes: &[u8]) -> String { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut out = String::new(); + for chunk in bytes.chunks(3) { + let mut buffer = [0u8; 3]; + buffer[..chunk.len()].copy_from_slice(chunk); + let value = u32::from_be_bytes([0, buffer[0], buffer[1], buffer[2]]); + for index in 0..4 { + if index <= chunk.len() { + out.push(ALPHABET[((value >> (18 - 6 * index)) & 63) as usize] as char); + } else { + out.push('='); + } + } + } + out +} + +fn unb64(text: &str) -> Vec { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut out = Vec::new(); + let symbols: Vec = text.bytes().filter(|byte| *byte != b'=').collect(); + for chunk in symbols.chunks(4) { + let mut value = 0u32; + for (index, symbol) in chunk.iter().enumerate() { + let digit = ALPHABET.iter().position(|c| c == symbol).unwrap() as u32; + value |= digit << (18 - 6 * index); + } + let bytes = value.to_be_bytes(); + out.extend_from_slice(&bytes[1..chunk.len()]); + } + out +} + +struct Device { + id: [u8; 16], + seed: [u8; 32], + signer: DeviceSigner, + encryption: HpkePrivateKey, +} + +impl Device { + fn new(tag: u8) -> Self { + let mut seed = [tag; 32]; + seed[0] ^= 0x5a; + Self { + id: [tag; 16], + seed, + signer: DeviceSigner::from_seed([tag; 16], &seed).unwrap(), + encryption: HpkePrivateKey::from_bytes(&[tag ^ 0x33; 32]).unwrap(), + } + } + + fn entry(&self) -> DeviceEntry { + DeviceEntry { + device_id: self.id, + signing_key: self.signer.public_key().try_into().unwrap(), + encryption_key: *self.encryption.public_key().as_bytes(), + status: DeviceStatus::Active, + } + } + + fn fixture(&self) -> DeviceFixture { + DeviceFixture { + id: hex(&self.id), + signing_seed: hex(&self.seed), + signing_key: hex(self.signer.public_key()), + encryption_secret: hex(self.encryption.expose_secret()), + encryption_key: hex(self.encryption.public_key().as_bytes()), + } + } +} + +fn generate() -> Fixture { + let vault_id = [0x11; 16]; + let generation = [0x22; 16]; + let profile = policy::profile_hash("org_fixture", "user_fixture"); + let recovery = RecoverySecret::from_bytes(&[0x44; 32]).unwrap(); + let a = Device::new(0xa1); + let b = Device::new(0xb2); + + let genesis_payload = PolicyPayload { + sequence: 0, + parent_hash: [0; 32], + profile_hash: profile, + epoch: 1, + operation: Operation::Genesis, + recovery_signing_key: recovery.signing_public_key().unwrap(), + recovery_encryption_key: *recovery.encryption_key().unwrap().public_key().as_bytes(), + devices: vec![a.entry()], + }; + let genesis = policy::encode_policy( + &policy::policy_binding(vault_id, generation, 1, a.id, [0; 32]), + &genesis_payload, + &a.signer, + ) + .unwrap(); + let state = MembershipState::from_genesis(&genesis, &vault_id, &generation, &profile).unwrap(); + + let mut add = state.next_payload(Operation::AddDevice); + add.devices.push(b.entry()); + let add_record = policy::encode_policy( + &policy::policy_binding(vault_id, generation, add.epoch, a.id, *state.hash()), + &add, + &a.signer, + ) + .unwrap(); + let added = state.apply(&add_record).unwrap(); + + let mut keyring = Keyring::new(); + keyring.insert(1, &[0x66; 32]).unwrap(); + let keyring_envelope = envelope::seal_keyring( + &added.envelope_binding(POLICY_OBJECT_ID, 1, a.id), + RecipientKind::Device, + &b.id, + &b.encryption.public_key(), + &keyring, + &a.signer, + ) + .unwrap(); + + let object_id = [0x77; 16]; + let object_binding = added.envelope_binding(object_id, 1, a.id); + let object_key = + ContentKey::from_bytes(KeyScope::from(&object_binding), [0x88; 16], &[0x99; 32]).unwrap(); + let object_envelope = envelope::wrap_object_key( + &object_binding, + keyring.epoch_key(1).unwrap(), + &object_key, + &a.signer, + ) + .unwrap(); + let chat_plaintext = b"fixture chat update: private canary text"; + let chat_record = content::seal( + &added.content_binding(object_id, a.id), + ContentPurpose::ChatUpdate, + &object_key, + &a.signer, + chat_plaintext, + 1024, + ) + .unwrap(); + + let lifecycle_plaintext = + br#"{"kind":"chats","id":"chat-1","op":"delete","hlc":"0000000000002-000000-dev-a"}"#; + let lifecycle_record = content::seal( + &added.content_binding(object_id, a.id), + ContentPurpose::RegistryLifecycle, + &object_key, + &a.signer, + lifecycle_plaintext, + 1024, + ) + .unwrap(); + + let mut revoke = added.next_payload(Operation::RevokeDevice); + revoke.devices[1].status = DeviceStatus::Revoked; + let revoke_record = policy::encode_policy( + &policy::policy_binding(vault_id, generation, revoke.epoch, a.id, *added.hash()), + &revoke, + &a.signer, + ) + .unwrap(); + let revoked = added.apply(&revoke_record).unwrap(); + + let pending = Device::new(0xc3); + let request = EnrollmentRequest { + vault_id, + request_id: [0x55; 16], + device_id: pending.id, + signing_key: pending.signer.public_key().try_into().unwrap(), + encryption_key: *pending.encryption.public_key().as_bytes(), + }; + let proof = request.sign(&pending.signer).unwrap(); + + Fixture { + vault_id: hex(&vault_id), + generation: hex(&generation), + org_id: "org_fixture".into(), + user_id: "user_fixture".into(), + profile_hash: hex(&profile), + recovery_secret: hex(recovery.expose_secret()), + recovery_kit: recovery.to_kit().to_string(), + recovery_signing_key: hex(&recovery.signing_public_key().unwrap()), + recovery_encryption_key: hex(recovery.encryption_key().unwrap().public_key().as_bytes()), + recovery_authority_id: hex(&state.recovery_authority_id()), + device_a: a.fixture(), + device_b: b.fixture(), + membership: vec![b64(&genesis), b64(&add_record), b64(&revoke_record)], + membership_hashes: vec![hex(state.hash()), hex(added.hash()), hex(revoked.hash())], + epochs_after: vec![state.epoch(), added.epoch(), revoked.epoch()], + keyring_envelope_b: b64(keyring_envelope.encoded()), + keyring_epoch_1: hex(keyring.epoch_key(1).unwrap()), + object_id: hex(&object_id), + object_key_envelope: b64(object_envelope.encoded()), + object_key_id: hex(object_key.identifier()), + object_key: hex(object_key.expose_secret()), + chat_record: b64(chat_record.encoded()), + chat_plaintext: String::from_utf8(chat_plaintext.to_vec()).unwrap(), + registry_lifecycle_record: b64(lifecycle_record.encoded()), + registry_lifecycle_plaintext: String::from_utf8(lifecycle_plaintext.to_vec()).unwrap(), + enrollment: EnrollmentFixture { + request_id: hex(&request.request_id), + device_id: hex(&request.device_id), + signing_key: hex(&request.signing_key), + encryption_key: hex(&request.encryption_key), + proof: hex(&proof), + pairing_code: request.pairing_code(state.genesis_hash()), + }, + } +} + +#[test] +fn vault_fixture_round_trips_across_the_control_plane() { + if std::env::var_os("ZERON_WRITE_VAULT_FIXTURE").is_some() { + let fixture = generate(); + let mut json = serde_json::to_string_pretty(&fixture).unwrap(); + json.push('\n'); + std::fs::write(FIXTURE, json).unwrap(); + } + let fixture: Fixture = + serde_json::from_str(&std::fs::read_to_string(FIXTURE).unwrap()).unwrap(); + let vault_id = unhex::<16>(&fixture.vault_id); + let generation = unhex::<16>(&fixture.generation); + let profile = policy::profile_hash(&fixture.org_id, &fixture.user_id); + assert_eq!(hex(&profile), fixture.profile_hash); + + // Membership chain verifies from the pinned genesis. + let records: Vec> = fixture.membership.iter().map(|r| unb64(r)).collect(); + let mut state = + MembershipState::from_genesis(&records[0], &vault_id, &generation, &profile).unwrap(); + assert_eq!(hex(state.hash()), fixture.membership_hashes[0]); + for (index, record) in records.iter().enumerate().skip(1) { + state = state.apply(record).unwrap(); + assert_eq!(hex(state.hash()), fixture.membership_hashes[index]); + assert_eq!(state.epoch(), fixture.epochs_after[index]); + } + let a_id = unhex::<16>(&fixture.device_a.id); + let b_id = unhex::<16>(&fixture.device_b.id); + assert!(state.active_device(&a_id).is_some()); + assert!(state.active_device(&b_id).is_none()); + assert_eq!( + hex(&state.recovery_authority_id()), + fixture.recovery_authority_id + ); + + // Recovery kit text reproduces the secret and its derived public keys. + let recovery = RecoverySecret::from_kit(&fixture.recovery_kit).unwrap(); + assert_eq!(hex(recovery.expose_secret()), fixture.recovery_secret); + assert_eq!( + hex(&recovery.signing_public_key().unwrap()), + fixture.recovery_signing_key + ); + + // Device B opens its keyring envelope under the head that issued it. + let added = MembershipState::from_genesis(&records[0], &vault_id, &generation, &profile) + .unwrap() + .apply(&records[1]) + .unwrap(); + let b_key = + HpkePrivateKey::from_bytes(&unhex::<32>(&fixture.device_b.encryption_secret)).unwrap(); + let a_public = unhex::<32>(&fixture.device_a.signing_key); + let keyring = envelope::open_keyring( + &unb64(&fixture.keyring_envelope_b), + &added.envelope_binding(POLICY_OBJECT_ID, 1, a_id), + RecipientKind::Device, + &b_id, + &b_key, + &a_public, + ) + .unwrap(); + assert_eq!(hex(keyring.epoch_key(1).unwrap()), fixture.keyring_epoch_1); + + // The object key unwraps under epoch 1 and opens the chat record. + let object_id = unhex::<16>(&fixture.object_id); + let object_key = envelope::unwrap_object_key( + &unb64(&fixture.object_key_envelope), + &added.envelope_binding(object_id, 1, a_id), + keyring.epoch_key(1).unwrap(), + &a_public, + ) + .unwrap(); + assert_eq!(hex(object_key.identifier()), fixture.object_key_id); + let opened = content::open( + &unb64(&fixture.chat_record), + &added.content_binding(object_id, a_id), + ContentPurpose::ChatUpdate, + &object_key, + &a_public, + 1024, + ) + .unwrap(); + assert_eq!( + opened.plaintext().as_bytes(), + fixture.chat_plaintext.as_bytes() + ); + // After the revocation the head epoch moved: an old-epoch record no + let lifecycle = content::open( + &unb64(&fixture.registry_lifecycle_record), + &added.content_binding(object_id, a_id), + ContentPurpose::RegistryLifecycle, + &object_key, + &a_public, + 1024, + ) + .unwrap(); + assert_eq!( + lifecycle.plaintext().as_bytes(), + fixture.registry_lifecycle_plaintext.as_bytes() + ); + // A lifecycle proof never opens as a field value (purpose is bound). + assert!( + content::open( + &unb64(&fixture.registry_lifecycle_record), + &added.content_binding(object_id, a_id), + ContentPurpose::RegistryField, + &object_key, + &a_public, + 1024, + ) + .is_err() + ); + // longer matches the current content binding. + assert!( + content::open( + &unb64(&fixture.chat_record), + &state.content_binding(object_id, a_id), + ContentPurpose::ChatUpdate, + &object_key, + &a_public, + 1024, + ) + .is_err() + ); + + // Enrollment proof and pairing code. + let request = EnrollmentRequest { + vault_id, + request_id: unhex::<16>(&fixture.enrollment.request_id), + device_id: unhex::<16>(&fixture.enrollment.device_id), + signing_key: unhex::<32>(&fixture.enrollment.signing_key), + encryption_key: unhex::<32>(&fixture.enrollment.encryption_key), + }; + request + .verify(&unhex::<64>(&fixture.enrollment.proof)) + .unwrap(); + assert_eq!( + request.pairing_code(added.genesis_hash()), + fixture.enrollment.pairing_code + ); +} diff --git a/crates/doc/src/registry.rs b/crates/doc/src/registry.rs index 769ff1f9e..72a461dbd 100644 --- a/crates/doc/src/registry.rs +++ b/crates/doc/src/registry.rs @@ -44,7 +44,7 @@ pub fn encode_hlc(ms: i64, counter: u32, device: &str) -> String { } /// `a` strictly newer than `b` (`None` = never written, loses to any). -fn hlc_newer(a: &str, b: Option<&str>) -> bool { +pub fn hlc_newer(a: &str, b: Option<&str>) -> bool { match b { None => true, Some(b) => a > b, @@ -95,10 +95,16 @@ pub struct RegistryRow { /// Per-field last-write clocks. #[serde(default)] pub clocks: BTreeMap, + /// Lifecycle proof for the tombstone (RFC 0001 §9): the sealed record a + /// member authored for exactly this row and `del_hlc`. Encrypted + /// readers accept a tombstone only with a verified proof; cleared on + /// revival. Opaque to the relay. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub del_proof: Option, } impl RegistryRow { - fn tombstone(kind: &str, id: &str, hlc: String) -> Self { + fn tombstone(kind: &str, id: &str, hlc: String, proof: Option) -> Self { Self { kind: kind.to_string(), id: id.to_string(), @@ -107,11 +113,12 @@ impl RegistryRow { del_hlc: Some(hlc), fields: BTreeMap::new(), clocks: BTreeMap::new(), + del_proof: proof, } } /// The newest clock anywhere on the row (delete-vs-live comparison base). - fn max_clock(&self) -> Option<&str> { + pub fn max_clock(&self) -> Option<&str> { let mut max = self.del_hlc.as_deref(); for clock in self.clocks.values() { if max.is_none_or(|m| clock.as_str() > m) { @@ -149,6 +156,10 @@ pub struct RowOp { /// clocks so recovery never coarsens causality. #[serde(default, skip_serializing_if = "Option::is_none")] pub clocks: Option>, + /// Lifecycle proof for a `delete` op (becomes the tombstone's + /// `del_proof`). Only meaningful on deletes; required in encrypted rooms. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proof: Option, } impl RowOp { @@ -168,7 +179,12 @@ pub fn apply_op(row: Option<&RegistryRow>, op: &RowOp) -> (Option, return match row { // Tombstone-on-missing guards against a late create racing the delete. None => ( - Some(RegistryRow::tombstone(&op.kind, &op.id, op.hlc.clone())), + Some(RegistryRow::tombstone( + &op.kind, + &op.id, + op.hlc.clone(), + op.proof.clone(), + )), true, ), Some(row) => { @@ -181,6 +197,8 @@ pub fn apply_op(row: Option<&RegistryRow>, op: &RowOp) -> (Option, let mut gone = row.clone(); gone.deleted = true; gone.del_hlc = Some(op.hlc.clone()); + // The proof travels with the tombstone it authorizes. + gone.del_proof = op.proof.clone(); gone.fields.clear(); gone.clocks.clear(); (Some(gone), true) @@ -204,6 +222,7 @@ pub fn apply_op(row: Option<&RegistryRow>, op: &RowOp) -> (Option, del_hlc: None, fields: BTreeMap::new(), clocks: BTreeMap::new(), + del_proof: None, } } Some(row) if row.deleted => { @@ -220,6 +239,7 @@ pub fn apply_op(row: Option<&RegistryRow>, op: &RowOp) -> (Option, del_hlc: row.del_hlc.clone(), fields: BTreeMap::new(), clocks: BTreeMap::new(), + del_proof: None, } } Some(row) => row.clone(), @@ -263,6 +283,7 @@ pub fn row_to_seed_op(row: &RegistryRow) -> RowOp { .clone() .unwrap_or_else(|| encode_hlc(0, 0, "seed")), clocks: None, + proof: row.del_proof.clone(), }; } RowOp { @@ -280,6 +301,7 @@ pub fn row_to_seed_op(row: &RegistryRow) -> RowOp { .map(str::to_string) .unwrap_or_else(|| encode_hlc(0, 0, "seed")), clocks: Some(row.clocks.clone()), + proof: None, } } @@ -326,6 +348,8 @@ struct PersistedState { /// pre-epoch snapshots default to 0. #[serde(default)] resync_epoch: u32, + #[serde(default)] + encrypted: bool, device_id: String, server_seq: u64, gc_floor: u64, @@ -337,6 +361,7 @@ struct PersistedState { /// The local registry replica. Pure data — no I/O, no async; the transport /// (`zeron_sync::RegistryClient`) and the engine host drive it under a lock. pub struct RegistryDoc { + encrypted: bool, device_id: String, /// kind → id → row (server truth). authoritative: HashMap>, @@ -352,6 +377,7 @@ pub struct RegistryDoc { impl RegistryDoc { pub fn new(device_id: impl Into) -> Self { Self { + encrypted: false, device_id: device_id.into(), authoritative: HashMap::new(), server_seq: 0, @@ -380,6 +406,29 @@ impl RegistryDoc { self.pending.len() } + /// Registry generations have independent cursors. Preserve local metadata + /// and clocks, but resubmit it through the encrypted codec on first entry. + /// Transcript/history migration is deliberately separate. + pub fn enter_encrypted_room(&mut self) -> bool { + if self.encrypted { + return false; + } + self.encrypted = true; + self.server_seq = 0; + self.gc_floor = 0; + let seeds: Vec = self + .authoritative + .values() + .flat_map(|rows| rows.values()) + .map(row_to_seed_op) + .collect(); + if !seeds.is_empty() { + self.enqueue_ops(seeds); + } + self.generation += 1; + true + } + // ── persistence ───────────────────────────────────────────────────────── // (see PersistedState::resync_epoch) @@ -393,6 +442,7 @@ impl RegistryDoc { let state = PersistedState { v: 1, resync_epoch: CURRENT_RESYNC_EPOCH, + encrypted: self.encrypted, device_id: self.device_id.clone(), server_seq: self.server_seq, gc_floor: self.gc_floor, @@ -412,6 +462,7 @@ impl RegistryDoc { ))); } let mut doc = Self::new(device_id); + doc.encrypted = state.encrypted; // One-shot healing resync: snapshots from before a cursor-integrity // fix may have a cursor that JUMPED past rows this replica never // applied (the ack/gap bugs above) — those rows are invisible and no @@ -523,6 +574,16 @@ impl RegistryDoc { true } + /// Keep the sync cursor at or below `cursor`: rows withheld before apply + /// (an encrypted profile waiting for keys) must be re-delivered by the + /// next pull/hello, so the cursor may not claim them (RFC 0001 §9). + pub fn hold_cursor(&mut self, cursor: u64) { + if self.server_seq > cursor { + self.server_seq = cursor; + self.generation += 1; + } + } + /// Retire an acked batch; returns whether it existed. /// /// Deliberately does NOT advance the sync cursor: the ack's `seq` is OUR @@ -565,6 +626,10 @@ impl RegistryDoc { } } + pub fn authoritative_row(&self, kind: &str, id: &str) -> Option<&RegistryRow> { + self.authoritative.get(kind)?.get(id) + } + fn put_authoritative(&mut self, row: RegistryRow) { self.authoritative .entry(row.kind.clone()) @@ -624,6 +689,7 @@ impl RegistryDoc { set: Some(set), hlc, clocks: None, + proof: None, }]); } @@ -638,6 +704,7 @@ impl RegistryDoc { set: None, hlc: hlc.clone(), clocks: None, + proof: None, }) .collect(); self.enqueue_ops(ops); @@ -710,12 +777,65 @@ impl RegistryDoc { ("id", json!(device.id)), ("name", json!(device.name)), ("platform", json!(device.platform)), + ("vaultDeviceId", opt_str(device.vault_device_id.as_deref())), ("lastSeenAt", opt_ms(device.last_seen_at)), ("createdAt", opt_ms(device.created_at)), ("version", opt_str(device.version.as_deref())), ("capabilities", json!(device.capabilities)), ]); self.write(KIND_DEVICES, &device.id.clone(), OpKind::Upsert, set); + if let Some(id) = &device.vault_device_id { + if !self.row_exists("vaultDevices", id) { + self.write( + "vaultDevices", + id, + OpKind::Upsert, + fields([ + ("name", json!(device.name)), + ("deviceId", json!(device.id)), + ("platform", json!(device.platform)), + ]), + ); + } + } + Ok(()) + } + + /// Display labels are encrypted registry metadata, separate from membership. + pub fn vault_device_names(&self) -> BTreeMap { + let mut names = BTreeMap::new(); + for device in self.read_devices().unwrap_or_default() { + if let Some(id) = device.vault_device_id { + names.insert(id, device.name); + } + } + for row in self.overlay_rows("vaultDevices") { + if let Some(name) = row.fields.get("name").and_then(Value::as_str) { + names.insert(row.id, name.to_owned()); + } + } + names + } + + pub fn rename_vault_device(&mut self, id: &str, name: &str) -> Result<(), DocError> { + let name = name.trim(); + if name.is_empty() || name.chars().count() > 80 { + return Err(DocError::Schema( + "Device names must contain 1–80 characters".into(), + )); + } + self.write( + "vaultDevices", + id, + OpKind::Upsert, + fields([("name", json!(name))]), + ); + let devices = self.read_devices()?; + for device in devices { + if device.vault_device_id.as_deref() == Some(id) { + self.rename_device(&device.id, name)?; + } + } Ok(()) } @@ -730,6 +850,16 @@ impl RegistryDoc { OpKind::Update, fields([("name", json!(name))]), ); + for alias in self.overlay_rows("vaultDevices") { + if alias.fields.get("deviceId").and_then(Value::as_str) == Some(device_id) { + self.write( + "vaultDevices", + &alias.id, + OpKind::Update, + fields([("name", json!(name))]), + ); + } + } Ok(true) } @@ -1205,6 +1335,7 @@ impl RegistryDoc { set: Some(set), hlc: encode_hlc(ms.max(1), 0, "migration"), clocks: None, + proof: None, }); }; for device in &state.devices { @@ -1217,6 +1348,7 @@ impl RegistryDoc { ("id", json!(device.id)), ("name", json!(device.name)), ("platform", json!(device.platform)), + ("vaultDeviceId", opt_str(device.vault_device_id.as_deref())), ("lastSeenAt", opt_ms(device.last_seen_at)), ("createdAt", opt_ms(device.created_at)), ("version", opt_str(device.version.as_deref())), diff --git a/crates/doc/src/registry/tests.rs b/crates/doc/src/registry/tests.rs index 6a8cd2502..1afa08e8d 100644 --- a/crates/doc/src/registry/tests.rs +++ b/crates/doc/src/registry/tests.rs @@ -30,6 +30,7 @@ fn upsert(set: &[(&str, Value)], at: i64) -> RowOp { ), hlc: hlc(at), clocks: None, + proof: None, } } @@ -45,6 +46,7 @@ fn update(set: &[(&str, Value)], hlc: String) -> RowOp { ), hlc, clocks: None, + proof: None, } } @@ -56,6 +58,7 @@ fn delete(at: i64) -> RowOp { set: None, hlc: hlc(at), clocks: None, + proof: None, } } @@ -239,6 +242,7 @@ fn wire_shapes_match_the_edge() { fn device(id: &str, name: &str) -> Device { Device { + vault_device_id: None, id: id.into(), name: name.into(), platform: "linux".into(), @@ -412,6 +416,7 @@ fn broadcast_seq_gap_applies_rows_but_holds_the_cursor() { .into_iter() .collect(), clocks: Default::default(), + del_proof: None, }; // Contiguous broadcast advances. assert!(ws.apply_rows(11, vec![row("chat-a", 11)])); @@ -490,6 +495,7 @@ fn future_harness_chat_rows_stay_visible_without_their_config() { del_hlc: None, fields, clocks: Default::default(), + del_proof: None, }], ); @@ -769,6 +775,7 @@ fn state_frames_delta_replace_and_reseed() { ), hlc: hlc_by(50, "dev-b"), clocks: None, + proof: None, }, ); let outcome = doc.apply_state(seq + 1, false, 0, vec![remote.clone()]); @@ -881,3 +888,52 @@ fn migration_seeds_pending_upserts_that_lose_to_live_writes() { Some("live rename") ); } + +#[test] +fn encryption_transition_resets_cursor_once_and_preserves_named_devices() { + let mut doc = RegistryDoc::new("laptop"); + let mut laptop = device("laptop", "Work laptop"); + laptop.vault_device_id = Some("aabbcc".into()); + doc.upsert_device(&laptop).unwrap(); + doc.apply_state(42, false, 10, vec![]); + assert!(doc.enter_encrypted_room()); + assert_eq!(doc.cursor(), 0); + assert_eq!(doc.read_devices().unwrap(), vec![laptop]); + doc.apply_state(7, false, 0, vec![]); + let mut restored = RegistryDoc::from_bytes(&doc.to_bytes().unwrap(), "laptop").unwrap(); + assert!(!restored.enter_encrypted_room()); + assert_eq!(restored.cursor(), 7); + assert_eq!( + restored.read_devices().unwrap()[0] + .vault_device_id + .as_deref(), + Some("aabbcc") + ); +} + +#[test] +fn vault_device_names_survive_reapproval_and_rename() { + let mut doc = RegistryDoc::new("laptop"); + let mut phone = device("phone", "iPhone"); + phone.vault_device_id = Some("old-identity".into()); + doc.upsert_device(&phone).unwrap(); + phone.vault_device_id = Some("new-identity".into()); + doc.upsert_device(&phone).unwrap(); + assert_eq!(doc.vault_device_names()["old-identity"], "iPhone"); + doc.rename_vault_device("new-identity", "Personal iPhone") + .unwrap(); + assert_eq!(doc.read_devices().unwrap()[0].name, "Personal iPhone"); + assert_eq!(doc.vault_device_names()["new-identity"], "Personal iPhone"); + doc.rename_vault_device("unmapped-removed-identity", "Old phone") + .unwrap(); + assert!(doc.rename_vault_device("new-identity", " ").is_err()); + let restored = RegistryDoc::from_bytes(&doc.to_bytes().unwrap(), "laptop").unwrap(); + assert_eq!( + restored.vault_device_names()["unmapped-removed-identity"], + "Old phone" + ); + assert_eq!( + restored.vault_device_names()["new-identity"], + "Personal iPhone" + ); +} diff --git a/crates/doc/src/workspace.rs b/crates/doc/src/workspace.rs index 7c06d43e2..9ce270469 100644 --- a/crates/doc/src/workspace.rs +++ b/crates/doc/src/workspace.rs @@ -103,6 +103,7 @@ impl WorkspaceDoc { row.insert("id", device.id.as_str())?; row.insert("name", device.name.as_str())?; row.insert("platform", device.platform.as_str())?; + set_opt_str(&row, "vaultDeviceId", device.vault_device_id.as_deref())?; set_opt_ms(&row, "lastSeenAt", device.last_seen_at)?; set_opt_ms(&row, "createdAt", device.created_at)?; set_opt_str(&row, "version", device.version.as_deref())?; @@ -591,6 +592,8 @@ fn dt(ms: i64) -> DateTime { #[serde(rename_all = "camelCase")] pub(crate) struct RawDevice { id: String, + #[serde(default)] + vault_device_id: Option, name: String, platform: String, #[serde(default)] @@ -606,6 +609,7 @@ pub(crate) struct RawDevice { impl From for Device { fn from(raw: RawDevice) -> Self { Device { + vault_device_id: raw.vault_device_id, id: raw.id, name: raw.name, platform: raw.platform, @@ -770,6 +774,7 @@ mod tests { fn device(id: &str, name: &str) -> Device { Device { + vault_device_id: None, id: id.into(), name: name.into(), platform: "linux".into(), diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 30acbc5e9..00d979f3e 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -12,6 +12,7 @@ zeron-sync.workspace = true zeron-harness.workspace = true zeron-rpc.workspace = true zeron-update.workspace = true +zeron-crypto.workspace = true loro.workspace = true tokio.workspace = true futures.workspace = true diff --git a/crates/engine/src/chat2_host.rs b/crates/engine/src/chat2_host.rs index 9297337cf..f80855e03 100644 --- a/crates/engine/src/chat2_host.rs +++ b/crates/engine/src/chat2_host.rs @@ -7,26 +7,194 @@ //! the room cursor in one `save_snapshot_with_cursor` transaction, so a //! restored backup can never disagree with its own cursor — the root cause //! of the redownload-forever class the old s2 clients suffered. +//! +//! Encrypted profiles (RFC 0001 §8) add a [`ChatCodec`] in front of every +//! import and behind every export: rows, checkpoints, and frontiers are +//! signed+sealed content records opened against the vault's pinned +//! membership before Loro sees a byte, and local updates are sealed once +//! into the durable outbox (`DocsStore::persist_encrypted_batch`) before any +//! transport carries them. There is no plaintext fallback: an encrypted +//! room whose keys are missing pauses, it never downgrades. -use std::sync::Arc; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; use futures::future::BoxFuture; +use zeron_crypto::content::{self, ContentPurpose}; +use zeron_crypto::record::{RecordError, UnverifiedRecord}; use zeron_doc::SessionDoc; -use zeron_sync::chat_client::{ChatDocSink, CheckpointFetcher, RowImportOutcome}; -use zeron_sync::{DocsStore, SyncError}; +use zeron_sync::chat_client::{ApplyOutcome, ChatDocSink, CheckpointFetcher, MAX_PUSH_BYTES}; +use zeron_sync::{DocsStore, PendingEncryptedBatch, SyncError}; +use crate::EngineError; use crate::doc_host::EdgeConfig; +use crate::vault::{OpenFailure, VaultService}; /// Doc epoch stamped on every chat2-synced snapshot (docs/chat2-sync.md M1: /// thin docs are lineage epoch 2; M3 readers discard-and-adopt below it). pub const CHAT2_DOC_EPOCH: u32 = 2; +/// Doc epoch of a snapshot whose cursor belongs to the ENCRYPTED room +/// (RFC 0001 §12: a fresh storage generation per encrypted profile). A +/// stored cursor from a lower epoch names the plaintext room and is not +/// carried over. +pub const CHAT2_ENCRYPTED_DOC_EPOCH: u32 = 3; +/// Content bytes a sealed row may carry: the transport cap minus the signed +/// wrapper + encrypted-payload overhead (`content::PAYLOAD_OVERHEAD` 144 + +/// `record::MAX_OVERHEAD` 256), rounded down for headroom. +pub const MAX_SEALED_UPDATE_BYTES: usize = MAX_PUSH_BYTES - 512; +/// Sealed frontier budget (a Loro version vector is small; this is a bound, +/// not a target). +const MAX_FRONTIER_BYTES: usize = 64 * 1024; + +/// The room an encrypted profile's chat lives in: a distinct namespace from +/// the plaintext room so ciphertext and legacy rows never share a log, and +/// so the plaintext copy is retained (never silently deleted) for the +/// separate cleanup step. Stays within the edge's `[A-Za-z0-9_-]{1,128}`. +pub fn encrypted_room_id(chat_id: &str) -> String { + const SUFFIX: &str = "-e1"; + if chat_id.len() + SUFFIX.len() <= 128 { + format!("{chat_id}{SUFFIX}") + } else { + let digest = zeron_crypto::sha256(&[b"zeron/encrypted-room/v1\0", chat_id.as_bytes()]); + let hex: String = digest[..20].iter().map(|b| format!("{b:02x}")).collect(); + format!("e1-{hex}") + } +} + +/// Seal/open boundary for one chat object (RFC 0001 §7.7, §8). +#[derive(Clone)] +pub struct ChatCodec { + vault: VaultService, + object_id: [u8; 16], +} + +impl ChatCodec { + pub fn new(vault: VaultService, chat_id: &str) -> Self { + Self { + object_id: crate::vault::object_id_for("chat", chat_id), + vault, + } + } + + pub fn object_id(&self) -> [u8; 16] { + self.object_id + } + + pub fn vault(&self) -> &VaultService { + &self.vault + } + + /// Seal `plaintext` under the current epoch (publishing this object's + /// key first when needed). Sealing is async because the object key may + /// have to become durable on the control plane before first use. + pub async fn seal( + &self, + purpose: ContentPurpose, + plaintext: &[u8], + max_plaintext_bytes: usize, + ) -> Result { + let material = self.vault.seal_material(self.object_id).await?; + content::seal( + &material.binding, + purpose, + &material.key, + &material.signer, + plaintext, + max_plaintext_bytes, + ) + .map_err(|e| EngineError::Other(format!("seal: {e}"))) + } + + /// Open a record with cached keys only. `Err(outcome)` is the explicit + /// verified result for the cursor; `KeyUnavailable` also kicks a + /// background key refresh so the host can `resume` afterwards. + pub fn open( + &self, + purpose: ContentPurpose, + encoded: &[u8], + max_plaintext_bytes: usize, + ) -> Result, ApplyOutcome> { + let payload_limit = max_plaintext_bytes.saturating_add(144); + let parsed = UnverifiedRecord::parse(encoded, payload_limit).map_err(|err| match err { + RecordError::UnsupportedVersion | RecordError::UnsupportedKind => { + ApplyOutcome::Unsupported + } + _ => ApplyOutcome::AuthenticationFailed, + })?; + let binding = *parsed.untrusted_binding(); + let context = self + .vault + .open_material_cached(self.object_id, &binding) + .map_err(|failure| match failure { + OpenFailure::Unavailable | OpenFailure::KeyUnavailable => { + self.vault.spawn_key_refresh(self.object_id); + ApplyOutcome::KeyUnavailable + } + OpenFailure::NotAuthorized => ApplyOutcome::AuthenticationFailed, + })?; + let opened = content::open( + encoded, + &context.binding, + purpose, + &context.key, + &context.author_public_key, + max_plaintext_bytes, + ) + .map_err(|err| match err { + content::ContentError::UnsupportedFormat + | content::ContentError::UnsupportedSuite + | content::ContentError::UnsupportedPurpose => ApplyOutcome::Unsupported, + _ => ApplyOutcome::AuthenticationFailed, + })?; + Ok(opened.plaintext().as_bytes().to_vec()) + } +} + +impl ChatCodec { + /// Open with a network fetch of missing object keys (for on-demand + /// reads outside the cursor path, e.g. blob display). + pub async fn open_async( + &self, + purpose: ContentPurpose, + encoded: &[u8], + max_plaintext_bytes: usize, + ) -> Result, ApplyOutcome> { + let payload_limit = max_plaintext_bytes.saturating_add(144); + let parsed = UnverifiedRecord::parse(encoded, payload_limit) + .map_err(|_| ApplyOutcome::AuthenticationFailed)?; + let binding = *parsed.untrusted_binding(); + let context = self + .vault + .open_material(self.object_id, &binding) + .await + .map_err(|failure| match failure { + OpenFailure::Unavailable | OpenFailure::KeyUnavailable => { + ApplyOutcome::KeyUnavailable + } + OpenFailure::NotAuthorized => ApplyOutcome::AuthenticationFailed, + })?; + content::open( + encoded, + &context.binding, + purpose, + &context.key, + &context.author_public_key, + max_plaintext_bytes, + ) + .map(|opened| opened.plaintext().as_bytes().to_vec()) + .map_err(|_| ApplyOutcome::AuthenticationFailed) + } +} + +type ResumeHook = Arc; /// [`ChatDocSink`] over a live [`SessionDoc`] + the cursor-bearing store. /// /// Loro import of a remote row/checkpoint fires the doc's root subscription, /// so the transcript watch, command drain, and debounced UI publish all ride /// the existing change plumbing — this type only owns import + same-tx -/// persistence. +/// persistence (and, for encrypted rooms, the seal/open boundary). pub struct EngineChatSink { /// WEAK: the sink lives inside the handle's `ChatClient` for the /// client's whole life — a strong ref here kept @@ -37,6 +205,16 @@ pub struct EngineChatSink { doc: std::sync::Weak, store: Arc, chat_id: String, + codec: Option, + doc_epoch: u32, + /// Last cursor this sink persisted (the outbox commit reuses it so a + /// sealed batch's snapshot never regresses the verified cursor). + last_cursor: AtomicU64, + /// Outbox receipts by wire batch id, retired on ack/permanent rejection. + outbox: Mutex>, + /// Installed once the client exists: called when keys arrive so the + /// paused client backfills from its honest cursor. + resume: Mutex>, } impl EngineChatSink { @@ -45,13 +223,58 @@ impl EngineChatSink { doc: Arc::downgrade(doc), store, chat_id: chat_id.into(), + codec: None, + doc_epoch: CHAT2_DOC_EPOCH, + last_cursor: AtomicU64::new(0), + outbox: Mutex::new(HashMap::new()), + resume: Mutex::new(None), + } + } + + /// An encrypted-room sink: every import opens through `codec`, every + /// export seals through it, and snapshots carry the encrypted epoch. + pub fn encrypted( + doc: &Arc, + store: Arc, + chat_id: impl Into, + codec: ChatCodec, + initial_cursor: u64, + ) -> Self { + Self { + doc: Arc::downgrade(doc), + store, + chat_id: chat_id.into(), + codec: Some(codec), + doc_epoch: CHAT2_ENCRYPTED_DOC_EPOCH, + last_cursor: AtomicU64::new(initial_cursor), + outbox: Mutex::new(HashMap::new()), + resume: Mutex::new(None), + } + } + + pub fn codec(&self) -> Option<&ChatCodec> { + self.codec.as_ref() + } + + pub fn doc_epoch(&self) -> u32 { + self.doc_epoch + } + + pub fn set_resume_hook(&self, hook: ResumeHook) { + *lock(&self.resume) = Some(hook); + } + + /// Invoke the resume hook (the host saw keys/policy arrive). + pub fn resume(&self) { + if let Some(hook) = lock(&self.resume).clone() { + hook(); } } /// Export the CURRENT doc and persist it with `cursor` in one tx. - fn persist_with_cursor(&self, cursor: u64) { + fn persist_with_cursor(&self, cursor: u64) -> Result<(), ()> { let Some(doc) = self.doc.upgrade() else { - return; + return Ok(()); }; match doc.export_snapshot() { Ok(bytes) => { @@ -59,26 +282,185 @@ impl EngineChatSink { &self.chat_id, &bytes, cursor, - CHAT2_DOC_EPOCH, + self.doc_epoch, ) { tracing::warn!(chat = %self.chat_id, error = %err, - "chat2 sink: snapshot persist failed (will retry on next change)"); + "chat2 sink: snapshot persist failed (cursor held; will retry)"); + return Err(()); } + self.last_cursor.fetch_max(cursor, Ordering::Relaxed); + Ok(()) } Err(err) => { tracing::warn!(chat = %self.chat_id, error = %err, "chat2 sink: snapshot export failed"); + Err(()) + } + } + } + + /// Row bytes as Loro sees them: plaintext rooms pass through; encrypted + /// rooms open (or report why they cannot). + fn open_update(&self, bytes: &[u8]) -> Result, ApplyOutcome> { + match &self.codec { + None => Ok(bytes.to_vec()), + Some(codec) => codec.open(ContentPurpose::ChatUpdate, bytes, MAX_SEALED_UPDATE_BYTES), + } + } + + // ── encrypted outbox ───────────────────────────────────────────────────── + + /// Seal one local update and commit it (with the current snapshot and + /// verified cursor) to the durable outbox. Returns the wire batch id and + /// the immutable bytes to enqueue; retries must use exactly these. + pub async fn seal_and_queue(&self, plaintext: &[u8]) -> Result<(String, Vec), EngineError> { + let codec = self + .codec + .as_ref() + .ok_or_else(|| EngineError::Other("plaintext room has no outbox".into()))?; + if plaintext.len() > MAX_SEALED_UPDATE_BYTES { + return Err(EngineError::Other(format!( + "update of {} bytes exceeds the sealed row budget", + plaintext.len() + ))); + } + let sealed = codec + .seal( + ContentPurpose::ChatUpdate, + plaintext, + MAX_SEALED_UPDATE_BYTES, + ) + .await?; + let doc = self + .doc + .upgrade() + .ok_or_else(|| EngineError::Other("doc evicted".into()))?; + let snapshot = doc + .export_snapshot() + .map_err(|e| EngineError::Other(format!("snapshot export: {e}")))?; + let cursor = self.last_cursor.load(Ordering::Relaxed); + let receipt = self.store.persist_encrypted_batch( + &self.chat_id, + &snapshot, + cursor, + self.doc_epoch, + &sealed, + zeron_sync::MAX_ENCRYPTED_OUTBOX_BYTES, + )?; + let batch_id = batch_id_of(receipt.revision_id()); + let bytes = receipt.encoded().to_vec(); + lock(&self.outbox).insert(batch_id.clone(), receipt); + Ok((batch_id, bytes)) + } + + /// Every durable batch for this doc, ready to enqueue. Batches sealed + /// under a superseded policy/epoch are re-sealed under the current one + /// first (RFC 0001 §11: refresh policy, THEN re-encrypt queued work) and + /// the stale copy is retired; nothing is ever dropped silently. + pub async fn replay_outbox(&self) -> Vec<(String, Vec)> { + let Some(codec) = self.codec.as_ref() else { + return Vec::new(); + }; + let (Some(author), Some(public_key)) = ( + codec.vault().device_id(), + codec.vault().signing_public_key(), + ) else { + return Vec::new(); + }; + let current = codec.vault().current_content_binding(codec.object_id()); + let pending = match self.store.pending_encrypted_batches_for_doc( + &self.chat_id, + &author, + &public_key, + 128, + ) { + Ok(pending) => pending, + Err(err) => { + tracing::warn!(chat = %self.chat_id, error = %err, + "chat2 outbox: replay read failed; batches retained"); + return Vec::new(); + } + }; + let mut out = Vec::with_capacity(pending.len()); + for batch in pending { + let batch_id = batch_id_of(batch.revision_id()); + if Some(*batch.binding()) == current { + out.push((batch_id.clone(), batch.encoded().to_vec())); + lock(&self.outbox).insert(batch_id, batch); + continue; + } + // Stale policy: open our own record with historical keys and + // seal it again under the current head. Failure keeps the old + // batch in the outbox for a later attempt. + let plaintext = match codec.open( + ContentPurpose::ChatUpdate, + batch.encoded(), + MAX_SEALED_UPDATE_BYTES, + ) { + Ok(plaintext) => plaintext, + Err(outcome) => { + tracing::warn!(chat = %self.chat_id, ?outcome, + "chat2 outbox: stale batch cannot be re-sealed yet; retained"); + continue; + } + }; + match self.seal_and_queue(&plaintext).await { + Ok((new_id, bytes)) => { + if let Err(err) = self.store.acknowledge_encrypted_batch(&batch) { + tracing::warn!(chat = %self.chat_id, error = %err, + "chat2 outbox: stale batch retire failed"); + } + tracing::info!(chat = %self.chat_id, "chat2 outbox: re-sealed a stale batch"); + out.push((new_id, bytes)); + } + Err(err) => { + tracing::warn!(chat = %self.chat_id, error = %err, + "chat2 outbox: re-seal failed; batch retained"); + } + } + } + out + } + + fn retire(&self, batch_id: &str) { + let receipt = lock(&self.outbox).remove(batch_id); + if let Some(receipt) = receipt { + match self.store.acknowledge_encrypted_batch(&receipt) { + Ok(true) => {} + Ok(false) => tracing::debug!(chat = %self.chat_id, batch_id, + "chat2 outbox: batch already retired"), + Err(err) => tracing::warn!(chat = %self.chat_id, batch_id, error = %err, + "chat2 outbox: retire failed (will be replayed and deduped)"), } } } } +/// Wire batch id of a sealed record: its immutable revision id, hex. +pub fn batch_id_of(revision_id: &[u8; 16]) -> String { + revision_id.iter().map(|b| format!("{b:02x}")).collect() +} + +fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { + mutex + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + impl ChatDocSink for EngineChatSink { - fn apply_row(&self, bytes: &[u8], cursor: u64) -> RowImportOutcome { + fn apply_row(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome { let Some(doc) = self.doc.upgrade() else { - return RowImportOutcome::Applied; + return ApplyOutcome::Applied; + }; + let update = match self.open_update(bytes) { + Ok(update) => update, + Err(outcome) => { + tracing::warn!(chat = %self.chat_id, ?outcome, cursor, + "chat2 sink: row not opened; cursor held"); + return outcome; + } }; - match doc.doc().import(bytes) { + match doc.doc().import(&update) { Ok(status) => { if status.pending.is_some() { // Room sequence contiguity does not prove causal history @@ -86,38 +468,79 @@ impl ChatDocSink for EngineChatSink { // advancing its cursor would lose them after restart. tracing::warn!(chat = %self.chat_id, cursor, "chat2 sink: row parked on missing deps; requesting repair"); - return RowImportOutcome::PendingDependencies; + return ApplyOutcome::PendingDependencies; } } Err(err) => { - // Malformed remote bytes cost the row, never the doc (the same - // skip-not-fail rule as transcript reads). The cursor still - // advances: replaying a poison row forever is the wedge class. + // Malformed (but, for encrypted rooms, AUTHENTICATED) update + // bytes cost the row, never the doc — the same skip-not-fail + // rule as transcript reads. The cursor still advances: + // replaying a poison row forever is the wedge class. tracing::warn!(chat = %self.chat_id, error = %err, "chat2 sink: row import failed; skipping row"); } } - self.persist_with_cursor(cursor); - RowImportOutcome::Applied + match self.persist_with_cursor(cursor) { + Ok(()) => ApplyOutcome::Applied, + Err(()) => ApplyOutcome::StorageFailed, + } } - fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> Result<(), String> { - let doc = self.doc.upgrade().ok_or("doc evicted")?; - let status = doc - .doc() - .import(bytes) - .map_err(|e| format!("checkpoint import: {e}"))?; - if status.pending.is_some() { - return Err("checkpoint is missing causal dependencies".into()); + fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome { + let Some(doc) = self.doc.upgrade() else { + return ApplyOutcome::StorageFailed; + }; + let snapshot = match &self.codec { + None => bytes.to_vec(), + Some(codec) => match codec.open( + ContentPurpose::Checkpoint, + bytes, + content::MAX_PLAINTEXT_BYTES, + ) { + Ok(snapshot) => snapshot, + Err(outcome) => { + tracing::warn!(chat = %self.chat_id, ?outcome, + "chat2 sink: checkpoint not opened; cursor held"); + return outcome; + } + }, + }; + match doc.doc().import(&snapshot) { + Ok(status) if status.pending.is_some() => return ApplyOutcome::PendingDependencies, + Ok(_) => {} + Err(err) => { + tracing::warn!(chat = %self.chat_id, error = %err, "chat2 sink: checkpoint import failed"); + return ApplyOutcome::StorageFailed; + } + } + match self.persist_with_cursor(cursor) { + Ok(()) => ApplyOutcome::Applied, + Err(()) => ApplyOutcome::StorageFailed, } - self.persist_with_cursor(cursor); - Ok(()) } fn contains_frontier(&self, frontier: &[u8]) -> bool { let Some(doc) = self.doc.upgrade() else { return true; // evicted: claim contained so the client idles, not refetches }; + // Encrypted rooms: an unopenable frontier is NOT contained (fetch), + // never "already have it" (RFC 0001 §8). + let opened; + let frontier = match &self.codec { + None => frontier, + Some(codec) => match codec.open(ContentPurpose::Frontier, frontier, MAX_FRONTIER_BYTES) + { + Ok(bytes) => { + opened = bytes; + &opened + } + Err(outcome) => { + tracing::info!(chat = %self.chat_id, ?outcome, + "chat2 frontier not opened; fetching checkpoint"); + return false; + } + }, + }; // NOTE deliberately no empty-frontier shortcut: an empty payload on // a PRESENT checkpoint is unreadable provenance, not proof there is // nothing to fetch — that shortcut made every fresh reader of such a @@ -150,26 +573,36 @@ impl ChatDocSink for EngineChatSink { } fn advance_cursor(&self, cursor: u64) { - self.persist_with_cursor(cursor); + let _ = self.persist_with_cursor(cursor); + } + + fn acknowledged(&self, batch_id: &str) { + self.retire(batch_id); + } + + fn rejected(&self, batch_id: &str) { + // The ops stay in the local doc and reach peers via the next + // checkpoint; the immutable ciphertext has no further use. + self.retire(batch_id); } } -/// `GET /chat2/{chatId}/checkpoint` with Range resume — the fetcher half of +/// `GET /chat2/{room}/checkpoint` with Range resume — the fetcher half of /// the C1 client contract. Partial downloads resume at the byte offset where /// the previous attempt died (the DO serves 206), which is the entire point /// of checkpoint-over-HTTP on the 1.2 Mbps links this design targets. pub struct EdgeCheckpointFetcher { http: reqwest::Client, edge: EdgeConfig, - chat_id: String, + room_id: String, } impl EdgeCheckpointFetcher { - pub fn new(http: reqwest::Client, edge: EdgeConfig, chat_id: impl Into) -> Self { + pub fn new(http: reqwest::Client, edge: EdgeConfig, room_id: impl Into) -> Self { Self { http, edge, - chat_id: chat_id.into(), + room_id: room_id.into(), } } } @@ -181,7 +614,7 @@ impl CheckpointFetcher for EdgeCheckpointFetcher { let url = format!( "{}/chat2/{}/checkpoint", edge.url.trim_end_matches('/'), - self.chat_id + self.room_id ); Box::pin(async move { let mut got: Vec = Vec::new(); @@ -209,7 +642,9 @@ impl CheckpointFetcher for EdgeCheckpointFetcher { // attempts, and a Range against it would splice two different // blobs (the import fails and burns a whole redial cycle). // The DO stamps every response with the checkpoint's seq — - // on change, restart the download from byte 0. + // on change, restart the download from byte 0. (Encrypted + // checkpoints additionally authenticate the whole object + // before import, so a splice can never materialize.) let seq = res .headers() .get("x-chat2-checkpoint-seq") @@ -256,11 +691,11 @@ impl CheckpointFetcher for EdgeCheckpointFetcher { } /// Plain-HTTPS chat pull/push (the airplane-wifi transport): GET/POST -/// `/chat2/{id}/rows` with the same bearer auth the checkpoint fetcher uses. +/// `/chat2/{room}/rows` with the same bearer auth the checkpoint fetcher uses. pub struct EdgeChatTransport { http: reqwest::Client, edge: EdgeConfig, - chat_id: String, + room_id: String, device_id: String, } @@ -268,13 +703,13 @@ impl EdgeChatTransport { pub fn new( http: reqwest::Client, edge: EdgeConfig, - chat_id: impl Into, + room_id: impl Into, device_id: impl Into, ) -> Self { Self { http, edge, - chat_id: chat_id.into(), + room_id: room_id.into(), device_id: device_id.into(), } } @@ -283,7 +718,7 @@ impl EdgeChatTransport { format!( "{}/chat2/{}/rows", self.edge.url.trim_end_matches('/'), - self.chat_id + self.room_id ) } } @@ -403,6 +838,17 @@ mod frontier_tests { assert!(sink.contains_frontier(&vv)); let _ = std::fs::remove_dir_all(&dir); } + + #[test] + fn encrypted_room_ids_stay_within_the_edge_id_grammar() { + assert_eq!(encrypted_room_id("chat-1"), "chat-1-e1"); + let long = "x".repeat(130); + let id = encrypted_room_id(&long); + assert!(id.len() <= 128); + assert!(id.starts_with("e1-")); + assert_eq!(id, encrypted_room_id(&long)); + assert_ne!(id, encrypted_room_id(&"y".repeat(130))); + } } #[cfg(test)] @@ -428,16 +874,13 @@ mod tests { let store = Arc::new(DocsStore::open(dir.path()).unwrap()); let target = Arc::new(SessionDoc::from_doc(loro::LoroDoc::new())); let sink = EngineChatSink::new(&target, store.clone(), "chat"); - sink.persist_with_cursor(0); - assert_eq!( - sink.apply_row(&row, 1), - RowImportOutcome::PendingDependencies - ); + sink.persist_with_cursor(0).unwrap(); + assert_eq!(sink.apply_row(&row, 1), ApplyOutcome::PendingDependencies); let (_, cursor, _) = store.load_snapshot_with_cursor("chat").unwrap().unwrap(); assert_eq!(cursor, 0, "a restart must retry the invisible update"); - sink.apply_checkpoint(&checkpoint, 0).unwrap(); - assert_eq!(sink.apply_row(&row, 1), RowImportOutcome::Applied); + assert_eq!(sink.apply_checkpoint(&checkpoint, 0), ApplyOutcome::Applied); + assert_eq!(sink.apply_row(&row, 1), ApplyOutcome::Applied); let (snapshot, cursor, _) = store.load_snapshot_with_cursor("chat").unwrap().unwrap(); assert_eq!(cursor, 1); let restored = loro::LoroDoc::new(); @@ -446,7 +889,10 @@ mod tests { let incomplete = Arc::new(SessionDoc::from_doc(loro::LoroDoc::new())); let incomplete_sink = EngineChatSink::new(&incomplete, store.clone(), "incomplete"); - assert!(incomplete_sink.apply_checkpoint(&row, 1).is_err()); + assert_eq!( + incomplete_sink.apply_checkpoint(&row, 1), + ApplyOutcome::PendingDependencies + ); assert!( store .load_snapshot_with_cursor("incomplete") diff --git a/crates/engine/src/diff_sync.rs b/crates/engine/src/diff_sync.rs index 08fc5ec61..6e22dbcad 100644 --- a/crates/engine/src/diff_sync.rs +++ b/crates/engine/src/diff_sync.rs @@ -167,6 +167,11 @@ struct DiffSyncInner { /// upgraded Arc — the token cuts it so no sidecar HTTP outlives shutdown. cancel: CancellationToken, supervisor: Mutex>>, + /// Encrypted-sync vault (engine assembly): when the profile is enrolled, + /// the diff sidecar is sealed (purpose Diff) into the chat's encrypted + /// room or not published at all — the legacy plaintext `/diff` route is + /// never used for an encrypted profile (RFC 0001 §10). + vault: std::sync::OnceLock, } fn lock(mutex: &Mutex) -> MutexGuard<'_, T> { @@ -192,6 +197,11 @@ impl CheckoutDiffSync { Self::start_with_orphan_grace(repos, workspace, device_id, edge, REPAIR_INTERVAL) } + /// Wire the encrypted-sync vault (engine assembly, once). + pub fn set_vault(&self, vault: crate::vault::VaultService) { + let _ = self.inner.vault.set(vault); + } + /// [`CheckoutDiffSync::start`] with an explicit orphan grace — test hook so /// removal-after-grace is exercisable without waiting out [`REPAIR_INTERVAL`]. #[doc(hidden)] @@ -218,6 +228,7 @@ impl CheckoutDiffSync { turn_trees: Mutex::new(HashMap::new()), cancel: CancellationToken::new(), supervisor: Mutex::new(None), + vault: std::sync::OnceLock::new(), }), }; let task = tokio::spawn(diff_sync_task( @@ -668,19 +679,57 @@ async fn sync_entry(inner: &Arc, entry: &Arc) { truncated: snapshot.truncated, published_at: chrono::Utc::now().timestamp_millis(), }; - let url = format!("{}/diff/{}", edge.url.trim_end_matches('/'), chat.id); // Fresh bearer per request — never the boot-time snapshot. let Some(bearer) = edge.bearer().await else { tracing::debug!(chat = %chat.id, "diff-sync: sidecar skipped (signed out)"); continue; }; - let result = inner - .http - .post(&url) - .bearer_auth(&bearer) - .json(&sidecar) - .send() - .await; + let vault = inner.vault.get().filter(|vault| vault.is_enrolled()); + let result = if let Some(vault) = vault { + // Encrypted profile: seal, and address the encrypted room's + // sidecar slot. A vault that cannot seal publishes nothing. + let Ok(json) = serde_json::to_vec(&sidecar) else { + continue; + }; + let codec = crate::chat2_host::ChatCodec::new(vault.clone(), &chat.id); + let sealed = match codec + .seal( + zeron_crypto::content::ContentPurpose::Diff, + &json, + 4 * 1024 * 1024 - 1024, + ) + .await + { + Ok(sealed) => sealed, + Err(err) => { + tracing::debug!(chat = %chat.id, error = %err, + "diff-sync: sidecar not sealed; skipped"); + continue; + } + }; + let url = format!( + "{}/chat2/{}/diff", + edge.url.trim_end_matches('/'), + crate::chat2_host::encrypted_room_id(&chat.id) + ); + inner + .http + .put(&url) + .bearer_auth(&bearer) + .header("content-type", "application/octet-stream") + .body(sealed.encoded().to_vec()) + .send() + .await + } else { + let url = format!("{}/diff/{}", edge.url.trim_end_matches('/'), chat.id); + inner + .http + .post(&url) + .bearer_auth(&bearer) + .json(&sidecar) + .send() + .await + }; match result { Ok(response) if !response.status().is_success() => { tracing::debug!(chat = %chat.id, status = %response.status(), diff --git a/crates/engine/src/doc_host.rs b/crates/engine/src/doc_host.rs index c3041b0d7..b36d7fdae 100644 --- a/crates/engine/src/doc_host.rs +++ b/crates/engine/src/doc_host.rs @@ -41,6 +41,9 @@ use crate::sessions::{SessionsEngine, SteerOutcome}; use crate::workspace_host::WorkspaceHost; use crate::{EngineError, new_id, now_ms}; +mod migration; +pub use migration::{EncryptionPreparation, MigrationStatus}; + /// Debounce window for local snapshot saves after a doc change. const SNAPSHOT_DEBOUNCE_MS: u64 = 1_000; @@ -213,6 +216,10 @@ pub struct DocHostConfig { } struct DocHostInner { + migration_running: AtomicBool, + migration_lock: tokio::sync::Mutex<()>, + encryption_preparing: Arc, + migration_status: Mutex, store: Arc, config: DocHostConfig, /// Set-once (first wins), cleared by `shutdown_workers`: sessions and @@ -264,6 +271,9 @@ struct DocHostInner { /// Peer links (engine assembly, edge runtimes only) — the transport that /// pushes queued attachment bytes to a remote host. links: OnceLock>, + /// Encrypted-sync vault (engine assembly, account-scoped runtimes): the + /// gate every chat2 content path consults before serializing. + vault: OnceLock, /// Shared client for sidecar blob PUT/GET (30s timeout, uploads.rs /// discipline — diff_sync's untimed client hung on dead links). http: reqwest::Client, @@ -311,6 +321,10 @@ impl Drop for TransferProgressGuard<'_> { } } +/// Sealed sidecar plaintext budget (tail JSON / blob bodies): under the +/// edge's 4 MiB sidecar cap once the record overhead is added. +const MAX_SIDECAR_PLAINTEXT: usize = 4 * 1024 * 1024 - 1024; + /// How long raw degradation must persist before connectivity reports it. /// Room joins, idle-link wakes, and navigation dials resolve well under a /// second on healthy networks; real outages outlive this comfortably. Recovery @@ -533,6 +547,16 @@ pub struct ChatDocHandle { chat2_pending_local: Mutex>>, /// Local-update feed into the chat2 client (drop = unsubscribe). chat2_local_sub: Mutex>, + /// This handle was built for the ENCRYPTED room (RFC 0001 §8): local + /// updates go through the sealer/outbox, remote bytes through the codec, + /// and the plaintext room is never touched. Fixed at open; a change in + /// vault enrollment retires the handle (see `open`). + encrypted: bool, + /// The encrypted room's sink — created at open so the sealer can commit + /// batches before (and without) a live client. + chat2_sink: Mutex>>, + /// Wakes the sealer when `chat2_pending_local` has plaintext to seal. + seal_wake: Arc, /// Doc subscription (drop = unsubscribe) — bumps the change watch on every commit. _sub: loro::Subscription, } @@ -700,6 +724,10 @@ impl DocHost { pub fn new(store: Arc, config: DocHostConfig) -> Self { Self { inner: Arc::new(DocHostInner { + migration_running: AtomicBool::new(false), + migration_lock: tokio::sync::Mutex::new(()), + encryption_preparing: Arc::new(AtomicBool::new(false)), + migration_status: Mutex::new(MigrationStatus::default()), store, config, sessions: Mutex::new(None), @@ -718,6 +746,7 @@ impl DocHost { connectivity_grace: Mutex::new(DegradeGrace::default()), executing: Mutex::new(HashSet::new()), links: OnceLock::new(), + vault: OnceLock::new(), http: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() @@ -859,6 +888,21 @@ impl DocHost { let _ = self.inner.links.set(links); } + /// Wire the encrypted-sync vault (engine assembly). Until set, chat2 + /// transports run in the legacy plaintext mode. + pub fn set_vault(&self, vault: crate::vault::VaultService) { + let _ = self.inner.vault.set(vault); + } + + fn plaintext_transport_allowed(&self) -> bool { + !self.inner.encryption_preparing.load(Ordering::Acquire) + && !self.vault().is_some_and(|v| v.is_enrolled()) + } + + pub fn vault(&self) -> Option<&crate::vault::VaultService> { + self.inner.vault.get() + } + /// Re-evaluate every open chat's command queue NOW. Called after an /// upload commit lands bytes on this device: a Run deferred on those /// bytes (`pending://` refs not yet on disk) becomes executable the @@ -868,7 +912,10 @@ impl DocHost { lock(&self.inner.handles).values().cloned().collect(); for handle in handles { let host = self.clone(); - self.spawn_worker(async move { host.drain_commands(&handle).await }); + self.spawn_worker(async move { + host.drain_commands(&handle).await; + host.drain_queue(&handle).await; + }); } } @@ -878,6 +925,7 @@ impl DocHost { if self.inner.workspace.set(workspace).is_ok() { self.spawn_cutover_watcher(chats); self.spawn_migration_sweep(); + self.spawn_encrypted_history_migration(); } } @@ -898,6 +946,7 @@ impl DocHost { let mut attempted: HashMap = HashMap::new(); loop { tokio::time::sleep(TICK).await; + if !host.plaintext_transport_allowed() { continue; } let Some(edge) = host.inner.config.edge.clone() else { return; // edge-less engine: nothing to migrate onto }; @@ -1061,10 +1110,21 @@ impl DocHost { Some(row) => row.room_gen.unwrap_or(1), None => 2, }; + // Encrypted profile (RFC 0001 §8, §12): once this profile is enrolled + // in a vault, EVERY chat routes through its encrypted room — locked, + // pending-key, and revoked states pause rather than fall back. + let vault = self + .inner + .vault + .get() + .filter(|vault| vault.is_enrolled()) + .cloned(); + let encrypted = vault.is_some() && self.inner.config.edge.is_some(); { let mut handles = lock(&self.inner.handles); if let Some(handle) = handles.get(chat_id) { - let stale = registry_gen >= 2 && handle.room_gen < 2; + let stale = + (registry_gen >= 2 && handle.room_gen < 2) || handle.encrypted != encrypted; if (stale || handle.retired.load(Ordering::Relaxed)) && !self.pinned(handle) { // A seed flipped this chat under a cached fat handle // (review B1): drop it so this open converges onto the @@ -1089,7 +1149,29 @@ impl DocHost { // duplicate every message. Local epoch >= 2 forces the chat2 branch // and best-effort completes the flip. let stored = self.inner.store.load_snapshot_with_cursor(chat_id)?; + let legacy_snapshot = stored + .as_ref() + .is_some_and(|(_, _, epoch)| *epoch < crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH); let stored_epoch = stored.as_ref().map(|(_, _, e)| *e).unwrap_or(0); + if encrypted && legacy_snapshot { + // Opening a chat may stamp subsequent local saves as encrypted. + // Retain its source and original room generation before that happens. + let source_key = format!("__encrypted_history_migration_v1:source:{chat_id}"); + if !self.inner.store.has_snapshot(&source_key)? { + self.inner + .store + .save_snapshot(&source_key, &stored.as_ref().unwrap().0)?; + let source_gen = if stored_epoch >= crate::chat2_host::CHAT2_DOC_EPOCH { + 2 + } else { + registry_gen + }; + self.inner.store.save_snapshot( + &format!("{source_key}:generation"), + &source_gen.to_le_bytes(), + )?; + } + } let room_gen = if stored_epoch >= crate::chat2_host::CHAT2_DOC_EPOCH { if registry_gen < 2 && let Some(ws) = self.workspace() @@ -1105,7 +1187,39 @@ impl DocHost { let mut snapshot_len = 0usize; let mut chat2_cursor = 0u64; let mut requeue_commands: Vec = Vec::new(); - let doc = if room_gen >= 2 { + let room_gen = if encrypted { room_gen.max(2) } else { room_gen }; + let doc = if encrypted { + // The encrypted room is a fresh storage generation: the local + // doc (whatever lineage it has) is kept — it is this device's + // copy and seeds the room through the host's checkpoint — but + // only a cursor stamped for THIS room carries over. + match stored { + Some((bytes, cursor, epoch)) => { + snapshot_len = bytes.len(); + chat2_cursor = if epoch >= crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH { + cursor + } else { + 0 + }; + let raw = loro::LoroDoc::new(); + raw.import(&bytes) + .map_err(|e| EngineError::Other(format!("snapshot import failed: {e}")))?; + SessionDoc::from_doc(raw) + } + None => { + let doc = SessionDoc::init(chat_id)?; + if let Ok(snapshot) = doc.export_snapshot() { + let _ = self.inner.store.save_snapshot_with_cursor( + chat_id, + &snapshot, + 0, + crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH, + ); + } + doc + } + } + } else if room_gen >= 2 { match stored { Some((bytes, cursor, epoch)) if epoch >= crate::chat2_host::CHAT2_DOC_EPOCH => { snapshot_len = bytes.len(); @@ -1230,6 +1344,17 @@ impl DocHost { chat2: Mutex::new(None), chat2_pending_local: Mutex::new(Vec::new()), chat2_local_sub: Mutex::new(None), + encrypted, + chat2_sink: Mutex::new(vault.as_ref().map(|vault| { + Arc::new(crate::chat2_host::EngineChatSink::encrypted( + &doc, + self.inner.store.clone(), + chat_id, + crate::chat2_host::ChatCodec::new(vault.clone(), chat_id), + chat2_cursor, + )) + })), + seal_wake: Arc::new(tokio::sync::Notify::new()), _sub: sub, }); { @@ -1261,10 +1386,27 @@ impl DocHost { // pending buffer the join drains — nothing composed during // (or before) the dial is lost to the room. let weak_push = Arc::downgrade(&handle); + let vault_gate = self.inner.vault.get().cloned(); + let preparing = self.inner.encryption_preparing.clone(); let sub = doc .doc() .subscribe_local_update(Box::new(move |bytes: &Vec| { if let Some(handle) = weak_push.upgrade() { + if handle.encrypted { + // Encrypted room: plaintext never enters a + // client. The sealer seals + commits to the + // outbox, then enqueues the immutable bytes. + lock(&handle.chat2_pending_local).push(bytes.clone()); + handle.seal_wake.notify_one(); + return true; + } + if preparing.load(Ordering::Acquire) + || vault_gate.as_ref().is_some_and(|v| v.is_enrolled()) + { + // Retain local edits while migration retires this old transport. + lock(&handle.chat2).take(); + return true; + } // The buffer push happens WHILE HOLDING the client // lock (verify pass: releasing it between the None // check and the push let the join's store+drain @@ -1299,7 +1441,12 @@ impl DocHost { // transcript). Push the doc's full update log as the join's // first batch; once acked the cursor moves and this never // re-arms. - if chat2_cursor == 0 { + let checkpoint_seeded = self + .inner + .store + .has_snapshot(&format!("__encrypted_history_seed:{chat_id}")) + .unwrap_or(false); + if chat2_cursor == 0 && !(encrypted && (legacy_snapshot || checkpoint_seeded)) { match doc .doc() .export(loro::ExportMode::updates(&loro::VersionVector::default())) @@ -1314,6 +1461,9 @@ impl DocHost { } } } + if encrypted { + self.spawn_chat2_sealer(&handle); + } self.spawn_chat2_join(edge.clone(), &handle, chat2_cursor); } else { // Straggler gen-1 chat (the s2 client is gone — post-cutover, @@ -1336,6 +1486,78 @@ impl DocHost { Ok(handle) } + /// Encrypted-room sealer (RFC 0001 §7.3, §8): drains plaintext local + /// updates into immutable sealed batches — each committed to the durable + /// outbox together with the snapshot and verified cursor BEFORE any + /// transport sees it — then hands the bytes to the live client (or + /// leaves them for the join's outbox replay). A vault that cannot seal + /// right now (keys pending) keeps the updates buffered and retries on + /// the next wake or vault status change; nothing is sent in the clear. + fn spawn_chat2_sealer(&self, handle: &Arc) { + let weak = Arc::downgrade(handle); + let wake = handle.seal_wake.clone(); + let Some(vault) = self.inner.vault.get().cloned() else { + return; + }; + let host = self.clone(); + self.spawn_worker(async move { + let mut status = vault.watch_status(); + loop { + tokio::select! { + _ = wake.notified() => {} + changed = status.changed() => { + if changed.is_err() { + return; + } + } + } + let Some(handle) = weak.upgrade() else { return }; + let Some(sink) = lock(&handle.chat2_sink).clone() else { return }; + loop { + let pending: Vec> = + std::mem::take(&mut *lock(&handle.chat2_pending_local)); + if pending.is_empty() { + break; + } + let mut iter = pending.into_iter(); + let mut stalled: Option> = None; + for update in iter.by_ref() { + if update.len() > crate::chat2_host::MAX_SEALED_UPDATE_BYTES { + // Over the sealed row budget: the ops stay in the + // doc and reach peers through a checkpoint. + tracing::warn!(chat = %handle.chat_id, bytes = update.len(), + "chat2: local update exceeds the sealed row budget; checkpoint compensates"); + host.spawn_chat2_checkpoint(&handle, "oversized-update"); + continue; + } + match sink.seal_and_queue(&update).await { + Ok((batch_id, bytes)) => { + if let Some(client) = &*lock(&handle.chat2) { + client.enqueue_sealed(batch_id, bytes); + } + } + Err(err) => { + tracing::info!(chat = %handle.chat_id, error = %err, + "chat2: sealing deferred until the vault is ready"); + stalled = Some(update); + break; + } + } + } + if let Some(first) = stalled { + // Put the unsealed tail back IN ORDER ahead of anything + // that arrived meanwhile, then wait for a wake/status. + let rest: Vec> = std::iter::once(first).chain(iter).collect(); + let mut buffer = lock(&handle.chat2_pending_local); + let arrived = std::mem::take(&mut *buffer); + *buffer = rest.into_iter().chain(arrived).collect(); + break; + } + } + } + }); + } + /// chat2 relay join (docs/chat2-sync.md C3): deadline on every dial, /// capped jittered backoff, wake redial — and the client resolves only /// after full catch-up (checkpoint + rows), so "joined" here means @@ -1349,8 +1571,21 @@ impl DocHost { let weak = Arc::downgrade(handle); let host = self.clone(); let mut token_changes = edge.token_changes(); + let encrypted = handle.encrypted; + let encrypted_sink = lock(&handle.chat2_sink).clone(); + let vault = self.inner.vault.get().cloned(); + // The encrypted room is a separate namespace (RFC 0001 §12); the + // plaintext room keeps its (retained, never auto-deleted) copy. + let room = if encrypted { + crate::chat2_host::encrypted_room_id(&chat) + } else { + chat.clone() + }; self.spawn_worker(async move { - let sink = Arc::new(crate::chat2_host::EngineChatSink::new(&doc, store, chat.clone())); + let sink: Arc = match encrypted_sink { + Some(sink) => sink, + None => Arc::new(crate::chat2_host::EngineChatSink::new(&doc, store, chat.clone())), + }; // The sink holds only a Weak doc ref (a strong one made every // chat2 handle read as perma-pinned — LRU eviction dead); this // task's own strong ref dies when the join resolves. @@ -1358,9 +1593,9 @@ impl DocHost { let fetcher = Arc::new(crate::chat2_host::EdgeCheckpointFetcher::new( http, edge.clone(), - chat.clone(), + room.clone(), )); - let url = edge.room_url(format!("/chat2/{chat}/ws")); + let url = edge.room_url(format!("/chat2/{room}/ws")); let mut wake = zeron_sync::wake::subscribe(); // Sibling-dial successes end a backoff wait immediately, exactly // like the joined clients' own reconnect loops (chat_client.rs). @@ -1372,9 +1607,32 @@ impl DocHost { let mut online = zeron_sync::wake::subscribe_online(); let mut backoff = crate::workspace_host::JOIN_RETRY_BASE; loop { + if !encrypted && (host.inner.encryption_preparing.load(Ordering::Acquire) || vault.as_ref().is_some_and(|v| v.is_enrolled())) { return; } if weak.upgrade().is_none() { return; // evicted or purged while dialing } + // Vault gate (RFC 0001 §4.3): an encrypted room is joined + // only with usable keys. Locked / key-update-required / + // revoked all WAIT here — no transport, no plaintext. + if encrypted { + let Some(vault) = vault.as_ref() else { return }; + let mut status = vault.watch_status(); + let mut announced = false; + while !vault.is_ready() { + if !announced { + tracing::info!(chat = %chat, status = ?vault.status().phase, + "chat2: encrypted room join waiting for the vault"); + announced = true; + let vault = vault.clone(); + tokio::spawn(async move { + let _ = vault.refresh().await; + }); + } + if status.changed().await.is_err() || weak.upgrade().is_none() { + return; + } + } + } // Dual transport: WS dial + a plain-HTTPS pull/push seam // (rows GET / POST on the same bearer auth as the checkpoint // fetch) — bootstraps in ~1 RTT and keeps syncing at backoff @@ -1383,7 +1641,7 @@ impl DocHost { let transport = Arc::new(crate::chat2_host::EdgeChatTransport::new( host.inner.http.clone(), edge.clone(), - chat.clone(), + room.clone(), device.clone(), )); let dial = tokio::time::timeout( @@ -1408,7 +1666,60 @@ impl DocHost { }; let mut events = client.events(); let mut lifecycle_events = client.events(); - { + if encrypted { + // Keys arriving later resume the paused client + // from its honest cursor. + let weak_resume = weak.clone(); + sink.set_resume_hook(Arc::new(move || { + if let Some(handle) = weak_resume.upgrade() + && let Some(client) = &*lock(&handle.chat2) + { + client.resume(); + } + })); + // Durable outbox first (re-sealed under the + // current policy where needed), then the live + // sealer for anything buffered in memory. + let replay = sink.replay_outbox().await; + { + let mut client_slot = lock(&handle.chat2); + for (batch_id, bytes) in replay { + client.enqueue_sealed(batch_id, bytes); + } + *client_slot = Some(client); + } + handle.seal_wake.notify_one(); + // Vault status changes (keys fetched, membership + // advanced, revocation) drive resume / leave. + if let Some(vault) = vault.clone() { + let weak_status = weak.clone(); + let sink_status = sink.clone(); + let chat_status = chat.clone(); + host.clone().spawn_worker(async move { + let mut status = vault.watch_status(); + loop { + if status.changed().await.is_err() { + return; + } + let Some(handle) = weak_status.upgrade() else { return }; + if vault.is_ready() { + sink_status.resume(); + } else if !vault.is_enrolled() + || matches!( + vault.status().phase, + crate::vault::VaultPhase::Revoked + ) + { + tracing::warn!(chat = %chat_status, + "chat2: vault membership ended; leaving encrypted room"); + lock(&handle.chat2).take(); + lock(&handle.chat2_local_sub).take(); + return; + } + } + }); + } + } else { // Store + drain under ONE client-lock critical // section: the subscription pushes to the buffer // while holding this same lock, so every commit @@ -1422,7 +1733,7 @@ impl DocHost { } *client_slot = Some(client); } - tracing::info!(chat = %chat, "chat2 room joined (converged)"); + tracing::info!(chat = %chat, encrypted, "chat2 room joined (converged)"); // Bootstrap heal: a room with NO checkpoint can't // cover its rows' causal deps for cold readers — a // pre-0.1.34 first contact whose init batch never @@ -1669,6 +1980,9 @@ impl DocHost { edge.url.trim_end_matches('/'), chat_id ); + if !self.plaintext_transport_allowed() { + return Err("encryption transition stopped the legacy seed".into()); + } let res = self .inner .http @@ -1866,27 +2180,61 @@ impl DocHost { return; }; let chat_id = handle.chat_id.clone(); - // Tail publish: cheap, every quiesce tick. + let codec = lock(&handle.chat2_sink) + .as_ref() + .and_then(|sink| sink.codec().cloned()); + let room = if handle.encrypted { + crate::chat2_host::encrypted_room_id(&chat_id) + } else { + chat_id.clone() + }; + // Tail publish: cheap, every quiesce tick. Encrypted rooms seal it + // (purpose Tail) — the sidecar is content, not routing metadata. if let Ok(tail) = zeron_doc::materialize_tail(&handle.doc, now_ms(), zeron_doc::TAIL_MESSAGE_COUNT) && let Ok(body) = serde_json::to_vec(&tail) { let http = self.inner.http.clone(); let edge_tail = edge.clone(); - let chat = chat_id.clone(); + let room_tail = room.clone(); + let codec_tail = codec.clone(); + let encrypted = handle.encrypted; + let gate = self.clone(); self.spawn_worker(async move { let Some(bearer) = edge_tail.bearer().await else { return; }; + let (body, content_type) = if encrypted { + let Some(codec) = codec_tail else { return }; + match codec + .seal( + zeron_crypto::content::ContentPurpose::Tail, + &body, + MAX_SIDECAR_PLAINTEXT, + ) + .await + { + Ok(sealed) => (sealed.encoded().to_vec(), "application/octet-stream"), + Err(err) => { + tracing::debug!(error = %err, "chat2 tail: seal failed; not published"); + return; + } + } + } else { + (body, "application/json") + }; let url = format!( "{}/chat2/{}/tail", edge_tail.url.trim_end_matches('/'), - chat + room_tail ); + if !encrypted && !gate.plaintext_transport_allowed() { + return; + } let _ = http .put(&url) .bearer_auth(&bearer) - .header("content-type", "application/json") + .header("content-type", content_type) .body(body) .send() .await; @@ -1931,18 +2279,68 @@ impl DocHost { let seq_covered = stats.cursor; let http = self.inner.http.clone(); let weak_note = Arc::downgrade(handle); + let codec = lock(&handle.chat2_sink) + .as_ref() + .and_then(|sink| sink.codec().cloned()); + let encrypted = handle.encrypted; + let room = if encrypted { + crate::chat2_host::encrypted_room_id(&chat_id) + } else { + chat_id.clone() + }; + let gate = self.clone(); self.spawn_worker(async move { let Some(bearer) = edge.bearer().await else { in_flight.store(false, Ordering::Release); return; }; + // Encrypted rooms: snapshot and frontier are sealed as two + // records (purposes Checkpoint / Frontier) under the same key; + // the frontier travels in the header as before (RFC 0001 §7.4). + let (snapshot, frontier) = if encrypted { + let Some(codec) = codec else { + in_flight.store(false, Ordering::Release); + return; + }; + let sealed_snapshot = codec + .seal( + zeron_crypto::content::ContentPurpose::Checkpoint, + &snapshot, + zeron_crypto::content::MAX_PLAINTEXT_BYTES, + ) + .await; + let sealed_frontier = codec + .seal( + zeron_crypto::content::ContentPurpose::Frontier, + &frontier, + 64 * 1024, + ) + .await; + match (sealed_snapshot, sealed_frontier) { + (Ok(snapshot), Ok(frontier)) => { + (snapshot.encoded().to_vec(), frontier.encoded().to_vec()) + } + (Err(err), _) | (_, Err(err)) => { + tracing::warn!(chat = %chat_id, error = %err, + "chat2 checkpoint: seal failed; not posted"); + in_flight.store(false, Ordering::Release); + return; + } + } + } else { + (snapshot, frontier) + }; let url = format!( "{}/chat2/{}/checkpoint?seqCovered={}", edge.url.trim_end_matches('/'), - chat_id, + room, seq_covered ); let size = snapshot.len() as u64; + if !encrypted && !gate.plaintext_transport_allowed() { + in_flight.store(false, Ordering::Release); + return; + } match http .post(&url) .bearer_auth(&bearer) @@ -2914,6 +3312,12 @@ impl DocHost { /// /// One at a time by design: each send changes the status this reads. pub async fn drain_queue(&self, handle: &Arc) { + if !self.history_ready_for_commands(&handle.chat_id) + || self.inner.encryption_preparing.load(Ordering::Acquire) + || self.inner.migration_running.load(Ordering::Acquire) + { + return; + } let Some(sessions) = self.sessions() else { return; // executor not wired yet; the set_sessions kick re-drains }; @@ -3018,6 +3422,12 @@ impl DocHost { item: &QueuedMessage, send: QueueSend, ) -> Result<(), EngineError> { + if !self.history_ready_for_commands(&handle.chat_id) + || self.inner.encryption_preparing.load(Ordering::Acquire) + || self.inner.migration_running.load(Ordering::Acquire) + { + return Err(EngineError::Other("Waiting for chat history encryption to finish".into())); + } let Some(sessions) = self.sessions() else { return Err(EngineError::Other("sessions engine not wired".into())); }; @@ -3629,12 +4039,26 @@ impl DocHost { return; // bare sync callers (unit tests) skip rather than panic }; let http = self.inner.http.clone(); + // Encrypted profile: blobs are content (RFC 0001 §10) — sealed under + // the chat's key, purpose Blob, or not published at all. + let codec = self + .inner + .vault + .get() + .filter(|vault| vault.is_enrolled()) + .map(|vault| crate::chat2_host::ChatCodec::new(vault.clone(), chat_id)); + let blob_room = if codec.is_some() { + crate::chat2_host::encrypted_room_id(chat_id) + } else { + chat_id.to_owned() + }; let base = format!( "{}/blob/{}/{}", edge.url.trim_end_matches('/'), - chat_id, + blob_room, encode_part_segment(&payload.part_id) ); + let gate = self.clone(); self.spawn_worker_on(&runtime, async move { let Some(bearer) = edge.bearer().await else { return; // signed out; summary-only until the next session @@ -3652,6 +4076,25 @@ impl DocHost { { puts.push((format!("{base}.diff"), "application/json", json)); } + if let Some(codec) = &codec { + let mut sealed = Vec::with_capacity(puts.len()); + for (url, _, body) in puts { + match codec + .seal(zeron_crypto::content::ContentPurpose::Blob, &body, MAX_SIDECAR_PLAINTEXT) + .await + { + Ok(record) => { + sealed.push((url, "application/octet-stream", record.encoded().to_vec())) + } + Err(err) => { + tracing::debug!(error = %err, "tool sidecar: seal failed; not uploaded"); + return; + } + } + } + puts = sealed; + } + if codec.is_none() && !gate.plaintext_transport_allowed() { return; } for (url, content_type, body) in puts { let sent = http .put(&url) @@ -3684,6 +4127,8 @@ impl DocHost { .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') && !part.is_empty() + && part != "." + && part != ".." && part.len() <= 200 && part .bytes() @@ -3702,13 +4147,19 @@ impl DocHost { // segment for transport (PART_RE allows `#`, which a raw URL would // truncate as a fragment — the 2026-08-10 silent-collision bug). let (chat, part) = blob_ref.split_once('/').expect("validated above"); + let encrypted = self.inner.vault.get().is_some_and(|v| v.is_enrolled()); + let room = if encrypted { + crate::chat2_host::encrypted_room_id(chat) + } else { + chat.to_owned() + }; let url = format!( "{}/blob/{}/{}", edge.url.trim_end_matches('/'), - chat, + room, encode_part_segment(part) ); - let res = self + let mut res = self .inner .http .get(&url) @@ -3716,15 +4167,50 @@ impl DocHost { .send() .await .map_err(|e| EngineError::Other(format!("sidecar fetch failed: {e}")))?; + if encrypted && res.status() == reqwest::StatusCode::NOT_FOUND { + res = self + .inner + .http + .get(format!( + "{}/blob/{}/{}", + edge.url.trim_end_matches('/'), + chat, + encode_part_segment(part) + )) + .bearer_auth(&bearer) + .send() + .await + .map_err(|e| EngineError::Other(e.to_string()))?; + } if !res.status().is_success() { return Err(EngineError::Other(format!( "sidecar fetch: HTTP {}", res.status().as_u16() ))); } - res.text() + let bytes = res + .bytes() .await - .map_err(|e| EngineError::Other(format!("sidecar body read failed: {e}"))) + .map_err(|e| EngineError::Other(format!("sidecar body read failed: {e}")))?; + // Encrypted profile: the blob is a sealed record; an unopenable one + // is an error, never displayed as "the output" (no legacy fallback). + if let Some(vault) = self.inner.vault.get().filter(|vault| vault.is_enrolled()) { + let codec = crate::chat2_host::ChatCodec::new(vault.clone(), chat); + let plaintext = codec + .open_async( + zeron_crypto::content::ContentPurpose::Blob, + &bytes, + MAX_SIDECAR_PLAINTEXT, + ) + .await + .map_err(|outcome| { + EngineError::Other(format!("sidecar could not be verified: {outcome:?}")) + })?; + return String::from_utf8(plaintext) + .map_err(|_| EngineError::Other("sidecar is not text".into())); + } + String::from_utf8(bytes.to_vec()) + .map_err(|_| EngineError::Other("sidecar is not text".into())) } /// §2.2 writer discipline: we host a chat iff its workspace row's `deviceId` is @@ -3757,6 +4243,14 @@ impl DocHost { /// Drain pending commands (host-only): evaluate → mark processed BEFORE execute → /// execute → write the outcome as the sole outcome writer. pub async fn drain_commands(&self, handle: &Arc) { + if !self.history_ready_for_commands(&handle.chat_id) { + return; + } + if self.inner.encryption_preparing.load(Ordering::Acquire) + || self.inner.migration_running.load(Ordering::Acquire) + { + return; + } let Some(sessions) = self.sessions() else { return; // executor not wired yet (or retired); the set_sessions kick re-drains }; diff --git a/crates/engine/src/doc_host/migration.rs b/crates/engine/src/doc_host/migration.rs new file mode 100644 index 000000000..4d791389f --- /dev/null +++ b/crates/engine/src/doc_host/migration.rs @@ -0,0 +1,740 @@ +//! Copy retained history into encrypted rooms. Never deletes the source. +use super::*; +use loro::ToJson; +use serde::{Deserialize, Serialize}; +use zeron_crypto::content::{ContentPurpose, MAX_PLAINTEXT_BYTES}; +use zeron_sync::chat_frames::{self as wire, frame_type}; + +const JOURNAL: &str = "__encrypted_history_migration_v1"; +const MAX_DOWNLOAD: usize = 64 * 1024 * 1024; + +#[derive(Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MigrationStatus { + pub phase: String, + pub total: usize, + pub completed: usize, + pub error: Option, + pub legacy_retained: bool, +} + +pub struct EncryptionPreparation(DocHost); +impl Drop for EncryptionPreparation { + fn drop(&mut self) { + self.0.finish_encryption_preparation(); + } +} + +fn fail(error: impl std::fmt::Display) -> EngineError { + EngineError::Other(error.to_string()) +} + +impl DocHost { + pub(super) fn history_ready_for_commands(&self, chat: &str) -> bool { + if !self.vault().is_some_and(|v| v.is_enrolled()) { + return true; + } + let check = || -> Result { + if self + .inner + .store + .has_snapshot(&format!("__encrypted_history_seed:{chat}"))? + { + return Ok(true); + } + if self + .inner + .store + .has_snapshot(&format!("{JOURNAL}:source:{chat}"))? + { + return Ok(false); + } + let required: HashSet = self + .inner + .store + .load_snapshot("__plaintext_history_inventory")? + .map(|bytes| serde_json::from_slice(&bytes)) + .transpose() + .map_err(fail)? + .unwrap_or_default(); + if required.contains(chat) { + return Ok(false); + } + Ok(!self + .inner + .store + .load_snapshot_with_cursor(chat)? + .is_some_and(|(_, _, epoch)| epoch < crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH)) + }; + check().unwrap_or(false) + } + + pub fn history_migration_status(&self) -> MigrationStatus { + lock(&self.inner.migration_status).clone() + } + + pub fn prepare_encryption(&self) -> Result { + if lock(&self.inner.handles) + .values() + .any(|h| Arc::strong_count(&h.doc) > 1) + { + return Err(fail("Finish running sessions before enabling encryption.")); + } + if self.workspace().is_some_and(|ws| !ws.connected()) { + return Err(fail("Wait for workspace sync before enabling encryption.")); + } + if let Some(workspace) = self.workspace() { + let ids: HashSet = workspace + .watch_chats() + .borrow() + .iter() + .map(|c| c.id.clone()) + .collect(); + self.inner.store.save_snapshot( + "__plaintext_history_inventory", + &serde_json::to_vec(&ids).map_err(fail)?, + )?; + } + self.inner + .encryption_preparing + .store(true, Ordering::Release); + let guard = EncryptionPreparation(self.clone()); + self.retire_plaintext_handles()?; + Ok(guard) + } + + fn retire_plaintext_handles(&self) -> Result<(), EngineError> { + let handles: Vec<_> = lock(&self.inner.handles) + .values() + .filter(|h| !h.encrypted) + .cloned() + .collect(); + for handle in handles { + self.inner + .store + .save_snapshot(&handle.chat_id, &handle.doc.export_snapshot()?)?; + handle.retired.store(true, Ordering::Release); + lock(&handle.chat2).take(); + lock(&handle.chat2_local_sub).take(); + lock(&self.inner.handles).remove(&handle.chat_id); + } + Ok(()) + } + + pub fn finish_encryption_preparation(&self) { + let _ = self.retire_plaintext_handles(); + self.inner + .encryption_preparing + .store(false, Ordering::Release); + self.kick_drains(); + } + + pub fn start_history_migration(&self) { + if self.inner.migration_running.load(Ordering::Acquire) { + return; + } + let host = self.clone(); + self.spawn_worker(async move { + let _ = host.migrate_history().await; + }); + } + + pub(super) fn spawn_encrypted_history_migration(&self) { + let host = self.clone(); + self.spawn_worker(async move { + loop { + if host.vault().is_some_and(|v| v.is_ready()) { + let _ = host.migrate_history().await; + } + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + } + }); + } + + /// Explicit retry uses the same durable journal as automatic migration. + pub async fn migrate_history(&self) -> Result<(), EngineError> { + let _migration = self.inner.migration_lock.lock().await; + self.inner.migration_running.store(true, Ordering::Release); + struct Running<'a>(&'a AtomicBool); + impl Drop for Running<'_> { + fn drop(&mut self) { + self.0.store(false, Ordering::Release); + } + } + let _running = Running(&self.inner.migration_running); + let result = self.migrate_history_inner().await; + self.inner.migration_running.store(false, Ordering::Release); + if let Err(error) = &result { + let mut status = lock(&self.inner.migration_status); + status.phase = "paused".into(); + status.error = Some(error.to_string()); + } + self.kick_drains(); + result + } + + async fn migrate_history_inner(&self) -> Result<(), EngineError> { + let vault = self + .vault() + .filter(|v| v.is_ready()) + .ok_or_else(|| fail("Vault is not ready"))?; + let edge = self + .inner + .config + .edge + .as_ref() + .ok_or_else(|| fail("No sync server"))?; + let workspace = self + .workspace() + .ok_or_else(|| fail("Workspace is not ready"))?; + let chats = workspace.watch_chats().borrow().clone(); + let key = format!( + "{JOURNAL}:{}", + vault.status().genesis_hash.unwrap_or_default() + ); + let mut done: HashSet = self + .inner + .store + .load_snapshot(&key)? + .map(|bytes| serde_json::from_slice(&bytes)) + .transpose() + .map_err(fail)? + .unwrap_or_default(); + *lock(&self.inner.migration_status) = MigrationStatus { + phase: "copying".into(), + total: chats.len(), + completed: chats.iter().filter(|c| done.contains(&c.id)).count(), + error: None, + legacy_retained: true, + }; + let mut first_error = None; + for chat in chats { + if done.contains(&chat.id) { + continue; + } + if !vault.is_ready() { + return Err(fail("Waiting for encryption keys")); + } + if let Err(error) = self + .migrate_chat_history(&chat.id, chat.room_gen.unwrap_or(1), edge, vault) + .await + { + first_error.get_or_insert_with(|| fail(format!("Chat {}: {error}", chat.id))); + continue; + } + done.insert(chat.id); + self.inner + .store + .save_snapshot(&key, &serde_json::to_vec(&done).map_err(fail)?)?; + lock(&self.inner.migration_status).completed += 1; + } + if let Some(error) = first_error { + return Err(error); + } + let mut status = lock(&self.inner.migration_status); + status.phase = "complete".into(); + status.error = None; + Ok(()) + } + + async fn migrate_chat_history( + &self, + chat: &str, + generation: u32, + edge: &EdgeConfig, + vault: &crate::vault::VaultService, + ) -> Result<(), EngineError> { + if chat.is_empty() + || chat.len() > 125 + || !chat + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"_-".contains(&b)) + { + return Err(fail("Invalid legacy chat identifier")); + } + let source = loro::LoroDoc::new(); + let stored = self.inner.store.load_snapshot_with_cursor(chat)?; + // Keep immutable local source snapshots separate from the live encrypted doc. + let backup_key = format!("{JOURNAL}:source:{chat}"); + if let Some(bytes) = self.inner.store.load_snapshot(&backup_key)? { + source.import(&bytes).map_err(fail)?; + } + if let Some((bytes, _, epoch)) = &stored { + if *epoch < crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH { + source.import(bytes).map_err(fail)?; + } + } + let cached = lock(&self.inner.handles).get(chat).cloned(); + if let Some(handle) = &cached { + if !handle.encrypted { + if Arc::strong_count(&handle.doc) > 1 { + return Err(fail("Waiting for the running session to finish")); + } + source + .import(&handle.doc.export_snapshot()?) + .map_err(fail)?; + self.save_snapshot(handle); + handle.retired.store(true, Ordering::Release); + lock(&handle.chat2).take(); + lock(&handle.chat2_local_sub).take(); + lock(&self.inner.handles).remove(chat); + } + } + + let retained_generation = self + .inner + .store + .load_snapshot(&format!("{backup_key}:generation"))?; + let generation = if let Some(bytes) = retained_generation { + u32::from_le_bytes( + bytes + .try_into() + .map_err(|_| fail("Invalid retained history generation"))?, + ) + } else if stored + .as_ref() + .is_some_and(|(_, _, epoch)| *epoch >= crate::chat2_host::CHAT2_DOC_EPOCH) + { + generation.max(2) + } else { + generation + }; + if generation < 2 { + if let Some(bytes) = self + .migration_get(edge, &format!("/snapshot/{chat}"), true) + .await? + { + if !bytes.is_empty() { + source.import(&bytes).map_err(fail)?; + } + } + } else { + self.import_migration_room(edge, chat, &source, None) + .await?; + } + if source.oplog_vv().is_empty() { + let required: HashSet = self + .inner + .store + .load_snapshot("__plaintext_history_inventory")? + .map(|bytes| serde_json::from_slice(&bytes)) + .transpose() + .map_err(fail)? + .unwrap_or_default(); + if !required.contains(chat) { + if stored.as_ref().is_some_and(|(_, _, epoch)| { + *epoch >= crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH + }) { + return Ok(()); + } + let codec = crate::chat2_host::ChatCodec::new(vault.clone(), chat); + let existing = loro::LoroDoc::new(); + if self + .import_migration_room( + edge, + &crate::chat2_host::encrypted_room_id(chat), + &existing, + Some(&codec), + ) + .await? + .1 + { + return Ok(()); + } + } + return Err(fail( + "History is unavailable; bring its original device online and retry", + )); + } + let snapshot = source.export(loro::ExportMode::Snapshot).map_err(fail)?; + self.inner.store.save_snapshot(&backup_key, &snapshot)?; + let source_frontier = source.oplog_vv(); + let source_json = source.get_deep_value().to_json_value(); + let mut refs = HashSet::new(); + collect_blob_refs(&source_json, &mut refs); + let codec = crate::chat2_host::ChatCodec::new(vault.clone(), chat); + for reference in refs { + let (owner, part) = reference + .split_once('/') + .ok_or_else(|| fail("Invalid sidecar reference"))?; + if owner != chat + || part.is_empty() + || part == "." + || part == ".." + || part.len() > 200 + || !part + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"._:#~-".contains(&b)) + { + return Err(fail("Invalid sidecar reference")); + } + let path = format!("/blob/{chat}/{}", encode_part_segment(part)); + let bytes = self + .migration_get(edge, &path, true) + .await? + .ok_or_else(|| { + fail("Full tool output is unavailable. Open the original device and retry.") + })?; + // Existing branch builds may already have sealed this old path. + let plaintext = + if zeron_crypto::record::UnverifiedRecord::parse(&bytes, MAX_SIDECAR_PLAINTEXT) + .is_ok() + { + codec + .open_async(ContentPurpose::Blob, &bytes, MAX_SIDECAR_PLAINTEXT) + .await + .map_err(|e| fail(format!("Blob verification failed: {e:?}")))? + } else { + bytes + }; + std::str::from_utf8(&plaintext).map_err(|_| { + fail("Tool output is not valid text; retained the source for review") + })?; + if part.ends_with(".diff") { + serde_json::from_slice::(&plaintext).map_err(fail)?; + } + let target = format!( + "/blob/{}/{}", + crate::chat2_host::encrypted_room_id(chat), + encode_part_segment(part) + ); + self.copy_migration_content(edge, &codec, &target, ContentPurpose::Blob, &plaintext) + .await?; + } + let room = crate::chat2_host::encrypted_room_id(chat); + let target = loro::LoroDoc::new(); + let (cursor, _, expected_frontier) = self + .import_migration_room(edge, &room, &target, Some(&codec)) + .await?; + target.import(&snapshot).map_err(fail)?; + // Merge any local encrypted work, including pending commands, without + // rebuilding IDs or touching the processed-command ledger. + if let Some(handle) = lock(&self.inner.handles).get(chat).cloned() { + target + .import(&handle.doc.export_snapshot()?) + .map_err(fail)?; + } else if let Some((bytes, _, epoch)) = &stored { + if *epoch >= crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH { + target.import(bytes).map_err(fail)?; + } + } + let full = target.export(loro::ExportMode::Snapshot).map_err(fail)?; + let frontier = target.oplog_vv().encode(); + let sealed_frontier = codec + .seal(ContentPurpose::Frontier, &frontier, 64 * 1024) + .await?; + let sealed = codec + .seal(ContentPurpose::Checkpoint, &full, MAX_PLAINTEXT_BYTES) + .await?; + let bearer = edge.bearer().await.ok_or_else(|| fail("Signed out"))?; + let response = self + .inner + .http + .post(format!( + "{}/chat2/{room}/checkpoint?seqCovered={cursor}&refreshReaders=1", + edge.url.trim_end_matches('/') + )) + .bearer_auth(bearer) + .header( + "x-chat2-expected-frontier", + base64::engine::general_purpose::STANDARD.encode(expected_frontier), + ) + .header( + "x-chat2-frontier", + base64::engine::general_purpose::STANDARD.encode(sealed_frontier.encoded()), + ) + .body(sealed.encoded().to_vec()) + .send() + .await + .map_err(fail)?; + if !response.status().is_success() { + return Err(fail(format!("Checkpoint upload: {}", response.status()))); + } + let verified = loro::LoroDoc::new(); + self.import_migration_room(edge, &room, &verified, Some(&codec)) + .await?; + if !verified.oplog_vv().includes_vv(&source_frontier) { + return Err(fail("Encrypted copy does not cover the source history")); + } + let view = SessionDoc::from_doc(verified); + let tail = zeron_doc::materialize_tail(&view, now_ms(), 64)?; + let tail = serde_json::to_vec(&tail).map_err(fail)?; + self.copy_migration_content( + edge, + &codec, + &format!("/chat2/{room}/tail"), + ContentPurpose::Tail, + &tail, + ) + .await?; + let cached = lock(&self.inner.handles).get(chat).cloned(); + if let Some(handle) = cached { + handle.doc.doc().import(&full).map_err(fail)?; + self.inner + .store + .save_snapshot(chat, &handle.doc.export_snapshot()?)?; + handle.publish_messages(); + } else { + self.inner.store.save_snapshot_with_cursor( + chat, + &full, + cursor, + crate::chat2_host::CHAT2_ENCRYPTED_DOC_EPOCH, + )?; + } + self.inner + .store + .save_snapshot(&format!("__encrypted_history_seed:{chat}"), b"verified")?; + Ok(()) + } + + async fn migration_get( + &self, + edge: &EdgeConfig, + path: &str, + optional: bool, + ) -> Result>, EngineError> { + let bearer = edge.bearer().await.ok_or_else(|| fail("Signed out"))?; + let mut response = self + .inner + .http + .get(format!("{}{}", edge.url.trim_end_matches('/'), path)) + .bearer_auth(bearer) + .send() + .await + .map_err(fail)?; + if optional && response.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(None); + } + if !response.status().is_success() { + return Err(fail(format!("History download: {}", response.status()))); + } + if response + .content_length() + .is_some_and(|n| n > MAX_DOWNLOAD as u64) + { + return Err(fail("History object exceeds the migration size limit")); + } + let mut bytes = Vec::new(); + while let Some(chunk) = response.chunk().await.map_err(fail)? { + if bytes.len() + chunk.len() > MAX_DOWNLOAD { + return Err(fail("History object exceeds the migration size limit")); + } + bytes.extend_from_slice(&chunk); + } + Ok(Some(bytes)) + } + + async fn import_migration_room( + &self, + edge: &EdgeConfig, + room: &str, + doc: &loro::LoroDoc, + codec: Option<&crate::chat2_host::ChatCodec>, + ) -> Result<(u64, bool, Vec), EngineError> { + let mut cursor = 0; + let mut checkpoint_loaded = false; + let mut present = false; + let mut checkpoint_frontier: Option> = None; + for _ in 0..10_000 { + let bytes = self + .migration_get(edge, &format!("/chat2/{room}/rows?after={cursor}"), false) + .await? + .unwrap(); + let frames = decode_migration_frames(&bytes)?; + let first = frames + .first() + .ok_or_else(|| fail("Missing history state"))?; + if first.kind != frame_type::STATE { + return Err(fail("Invalid history state")); + } + if checkpoint_frontier + .as_ref() + .is_some_and(|previous| previous != &first.payload) + { + return Err(fail("History checkpoint changed; retry migration")); + } + checkpoint_frontier = Some(first.payload.clone()); + let state: wire::StateHeader = + serde_json::from_value(first.header.clone()).map_err(fail)?; + if state.checkpoint_seq > state.head_seq || state.seq_floor > state.head_seq { + return Err(fail("Invalid history revision bounds")); + } + present |= state.checkpoint_size > 0 || state.head_seq > 0; + if state.checkpoint_size > 0 && (!checkpoint_loaded || cursor < state.seq_floor) { + let mut cp = self + .migration_get(edge, &format!("/chat2/{room}/checkpoint"), false) + .await? + .unwrap(); + if let Some(codec) = codec { + cp = codec + .open_async(ContentPurpose::Checkpoint, &cp, MAX_PLAINTEXT_BYTES) + .await + .map_err(|e| fail(format!("Checkpoint verification failed: {e:?}")))?; + } + doc.import(&cp).map_err(fail)?; + checkpoint_loaded = true; + // Re-fetch at the boundary and require the same checkpoint + // frontier; concurrent replacements must restart this copy. + cursor = state.checkpoint_seq; + continue; + } + let before = cursor; + let mut ended = false; + for frame in frames.into_iter().skip(1) { + match frame.kind { + frame_type::ROW => { + let seq = frame + .header + .get("seq") + .and_then(serde_json::Value::as_u64) + .ok_or_else(|| fail("Invalid history row"))?; + if seq <= cursor { + continue; + } + if seq != cursor + 1 { + return Err(fail("History has a sequence gap; retry migration")); + } + let payload = if let Some(codec) = codec { + codec + .open_async( + ContentPurpose::ChatUpdate, + &frame.payload, + MAX_PLAINTEXT_BYTES, + ) + .await + .map_err(|e| fail(format!("History verification failed: {e:?}")))? + } else { + frame.payload + }; + doc.import(&payload).map_err(fail)?; + cursor = seq; + } + frame_type::ROWS_DONE => ended = true, + _ => return Err(fail("Unexpected history frame")), + } + } + if ended && cursor >= state.head_seq { + return Ok((cursor, present, checkpoint_frontier.unwrap_or_default())); + } + if cursor == before { + return Err(fail("History download made no progress")); + } + } + Err(fail("History pagination limit exceeded")) + } + + async fn copy_migration_content( + &self, + edge: &EdgeConfig, + codec: &crate::chat2_host::ChatCodec, + path: &str, + purpose: ContentPurpose, + bytes: &[u8], + ) -> Result<(), EngineError> { + if purpose == ContentPurpose::Blob { + if let Some(existing) = self.migration_get(edge, path, true).await? { + let opened = codec + .open_async(purpose, &existing, MAX_SIDECAR_PLAINTEXT) + .await + .map_err(|e| fail(format!("Existing sidecar verification failed: {e:?}")))?; + if opened == bytes { + return Ok(()); + } + return Err(fail( + "Encrypted tool output differs from its legacy copy; preserved both for review", + )); + } + } + let sealed = codec.seal(purpose, bytes, MAX_SIDECAR_PLAINTEXT).await?; + let bearer = edge.bearer().await.ok_or_else(|| fail("Signed out"))?; + let response = self + .inner + .http + .put(format!("{}{}", edge.url.trim_end_matches('/'), path)) + .bearer_auth(bearer) + .header("content-type", "application/octet-stream") + .body(sealed.encoded().to_vec()) + .send() + .await + .map_err(fail)?; + if !response.status().is_success() { + return Err(fail(format!("Sidecar upload: {}", response.status()))); + } + let saved = self.migration_get(edge, path, false).await?.unwrap(); + let opened = codec + .open_async(purpose, &saved, MAX_SIDECAR_PLAINTEXT) + .await + .map_err(|e| fail(format!("Sidecar verification failed: {e:?}")))?; + if opened != bytes { + return Err(fail("Sidecar read-back mismatch")); + } + Ok(()) + } +} + +fn decode_migration_frames(bytes: &[u8]) -> Result, EngineError> { + let mut offset = 0; + let mut frames = Vec::new(); + while offset < bytes.len() { + let length = bytes + .get(offset..offset + 4) + .ok_or_else(|| fail("Truncated history frame"))?; + let length = u32::from_le_bytes(length.try_into().unwrap()) as usize; + offset += 4; + let end = offset + .checked_add(length) + .ok_or_else(|| fail("Oversized history frame"))?; + let frame = wire::decode( + bytes + .get(offset..end) + .ok_or_else(|| fail("Truncated history frame"))?, + ) + .ok_or_else(|| fail("Invalid history frame"))?; + if frames.len() >= 50_000 { + return Err(fail("Too many history frames")); + } + frames.push(frame); + offset = end; + } + Ok(frames) +} + +fn collect_blob_refs(value: &serde_json::Value, refs: &mut HashSet) { + match value { + serde_json::Value::Object(fields) => { + for (key, value) in fields { + if matches!(key.as_str(), "outputRef" | "diffRef") { + if let Some(reference) = value.as_str() { + refs.insert(reference.to_owned()); + } + } else { + collect_blob_refs(value, refs); + } + } + } + serde_json::Value::Array(values) => { + for value in values { + collect_blob_refs(value, refs); + } + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn incomplete_or_malformed_history_streams_are_rejected() { + let frame = wire::encode(frame_type::STATE, &serde_json::json!({"headSeq": 0}), &[]); + let mut stream = (frame.len() as u32).to_le_bytes().to_vec(); + stream.extend(&frame); + assert_eq!(decode_migration_frames(&stream).unwrap().len(), 1); + for end in 1..stream.len() { + assert!(decode_migration_frames(&stream[..end]).is_err()); + } + stream.extend([1, 0]); + assert!(decode_migration_frames(&stream).is_err()); + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 58516c339..7f4d665c6 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -35,6 +35,7 @@ pub mod terminals; pub mod titles; pub mod uploads; pub mod workspace_files; +pub mod vault; pub mod workspace_host; pub use agent_accounts::{AgentAccounts, AgentAccountsConfig}; @@ -132,6 +133,8 @@ pub struct EngineCore { pub device_id: String, /// Local→synced profile import (account-scoped runtimes only). pub local_import: Option, + /// Encrypted-sync vault for this profile (unavailable on local scope). + pub vault: vault::VaultService, workspace_scope: WorkspaceScope, /// Auth service (attached by [`Engine::run`]; a lazy dev-mode instance otherwise). auth: std::sync::Mutex>, @@ -220,6 +223,37 @@ impl EngineCore { edge: edge.clone(), }, ); + // Encrypted-sync vault: device identity + keyring under the profile + // store, edge control-plane client only for account-scoped runtimes + // with an edge. Opening never fails the boot: a locked secure store + // is a reported state, not an error. + let vault = { + let client = edge + .as_ref() + .filter(|_| profile.scope() != WorkspaceScope::Local) + .map(|edge| { + vault::client::VaultClient::new( + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap_or_default(), + edge.clone(), + profile.org_id(), + ) + }); + let protection: Box = if client.is_some() { + vault::platform_protection() + } else { + Box::new(vault::LockedProtection("local profile".into())) + }; + let store = vault::VaultStore::new( + profile.store_root(), + format!("{}/{}", profile.org_id(), profile.user_id()), + protection, + ); + vault::VaultService::open(store, client, profile.org_id(), profile.user_id()) + }; + doc_host.set_vault(vault.clone()); let workspace = WorkspaceHost::open( store, WorkspaceHostConfig { @@ -228,6 +262,7 @@ impl EngineCore { platform: std::env::consts::OS.to_string(), org_id: profile.org_id().to_string(), user_id: profile.user_id().to_string(), + vault: Some(vault.clone()), edge: edge.clone(), }, )?; @@ -281,6 +316,7 @@ impl EngineCore { repos.clone(), )); let diff_sync = CheckoutDiffSync::start(repos.clone(), workspace.clone(), &device_id, edge); + diff_sync.set_vault(vault.clone()); // Turn starts snapshot the checkout tree — the "Latest turn" diff base. let turn_diff = diff_sync.clone(); sessions.set_turn_listener(Arc::new(move |chat_id, cwd| { @@ -302,6 +338,7 @@ impl EngineCore { agent_accounts, device_id, local_import, + vault, workspace_scope: profile.scope(), auth: std::sync::Mutex::new(None), links: std::sync::Mutex::new(None), @@ -401,8 +438,19 @@ impl EngineCore { /// re-reads auth on every (re)dial, so token refreshes take effect at reconnect. pub fn start_host_relay(&self, edge_url: &str) -> zeron_rpc::HostRelay { let auth = self.auth(); - let config = + let mut config = zeron_rpc::HostRelayConfig::new(edge_url, self.device_id.clone(), Arc::new(auth)); + // The authenticated device channel (RFC 0001 §10): peers that + // complete the Noise handshake against this vault's membership get + // the content surface even though the profile is enrolled; plaintext + // relay conns keep the gated service below. + config.channel = Some(zeron_rpc::ChannelHost { + authority: Arc::new(self.vault.clone()), + service: Arc::new(rpc::RelayRpc::secured( + self.rpc_service(), + self.vault.clone(), + )), + }); let doc_host = self.doc_host.clone(); let on_nudge: zeron_rpc::NudgeHandler = Arc::new(move |chat_id: String| { // Opening the doc joins its room + syncs; drain fires on the change @@ -414,7 +462,11 @@ impl EngineCore { } } }); - zeron_rpc::HostRelay::spawn(config, self.rpc_service(), on_nudge) + zeron_rpc::HostRelay::spawn( + config, + Arc::new(rpc::RelayRpc::new(self.rpc_service(), self.vault.clone())), + on_nudge, + ) } pub fn rpc_service(&self) -> Arc { @@ -442,6 +494,7 @@ impl EngineCore { if let Some(importer) = self.local_import.clone() { rpc = rpc.with_local_import(importer); } + rpc = rpc.with_vault(self.vault.clone()); Arc::new(rpc) } @@ -791,6 +844,9 @@ impl Engine { link_config.liveness = Some(Arc::new(move |device_id: &str| { workspace_for_liveness.peer_liveness(device_id) })); + // Enrolled profiles dial peers through the authenticated device + // channel only (RFC 0001 §10); the authority is the vault itself. + link_config.channel = Some(Arc::new(core.vault.clone())); let links = zeron_rpc::LinkCache::new(link_config); let links_for_presence = links.clone(); core.workspace diff --git a/crates/engine/src/rpc.rs b/crates/engine/src/rpc.rs index 131109b20..218e4a81e 100644 --- a/crates/engine/src/rpc.rs +++ b/crates/engine/src/rpc.rs @@ -478,6 +478,7 @@ pub struct EngineRpc { links: Option>, updater: Option, local_import: Option, + vault: Option, engine_info: EngineInfo, } @@ -518,10 +519,23 @@ impl EngineRpc { links: None, updater: None, local_import: None, + vault: None, engine_info, } } + /// Attach the encrypted-sync vault service (account-scoped runtimes). + pub fn with_vault(mut self, vault: crate::vault::VaultService) -> Self { + self.vault = Some(vault); + self + } + + fn vault(&self) -> Result<&crate::vault::VaultService, RpcError> { + self.vault + .as_ref() + .ok_or_else(|| RpcError::Failed("vault unavailable".into())) + } + /// Attach the auth service (AuthStatus + AuthRpc mutations). pub fn with_auth(mut self, auth: Auth) -> Self { self.auth = Some(auth); @@ -687,6 +701,9 @@ impl EngineRpc { method: &str, params: serde_json::Value, ) -> Result { + // An enrolled profile's link cache dials through the authenticated + // device channel or not at all (`LinkCacheConfig::channel`), so no + // plaintext gate is needed here. let Some(links) = &self.links else { return Err(RpcError::Failed(format!( "cannot reach device {target}: remote routing unavailable (offline)" @@ -1155,6 +1172,103 @@ impl RpcService for AuthRpc { } } +/// The RPC surface served to relay clients. Two instances exist per engine: +/// the PLAINTEXT one (served on ordinary relay frames) refuses everything +/// the moment the profile is enrolled, and the SECURED one (served only on +/// conns that completed the authenticated device channel) carries content +/// for enrolled profiles. Both refuse vault management: that stays local. +pub(crate) struct RelayRpc { + service: std::sync::Arc, + vault: crate::vault::VaultService, + secured: bool, +} + +impl RelayRpc { + pub(crate) fn new( + service: std::sync::Arc, + vault: crate::vault::VaultService, + ) -> Self { + Self { + service, + vault, + secured: false, + } + } + + pub(crate) fn secured( + service: std::sync::Arc, + vault: crate::vault::VaultService, + ) -> Self { + Self { + service, + vault, + secured: true, + } + } + + fn plaintext_refused(&self) -> bool { + !self.secured && self.vault.is_enrolled() + } +} + +#[async_trait] +impl RpcService for RelayRpc { + async fn handle(&self, method: &str, params: serde_json::Value) -> Result { + if method.starts_with("Vault") { + return Err(RpcError::Failed( + "vault operations require a local connection".into(), + )); + } + let mut status = self.vault.watch_status(); + let request = self.service.handle(method, params); + tokio::pin!(request); + let reply = loop { + if self.plaintext_refused() { + return Err(RpcError::Failed("encrypted device channel required".into())); + } + tokio::select! { + biased; + changed = status.changed() => { + if changed.is_err() { + return Err(RpcError::Closed); + } + } + reply = &mut request => break reply?, + } + }; + if self.plaintext_refused() { + return Err(RpcError::Failed("encrypted device channel required".into())); + } + match reply { + RpcReply::Value(value) => Ok(RpcReply::Value(value)), + RpcReply::Stream(stream) => { + let vault = self.vault.clone(); + let secured = self.secured; + let stream = futures::stream::unfold( + (stream, vault, status), + move |(mut stream, vault, mut status)| async move { + loop { + if !secured && vault.is_enrolled() { + return None; + } + tokio::select! { + biased; + changed = status.changed() => { + if changed.is_err() { + return None; + } + } + item = stream.next() => return item.map(|item| (item, (stream, vault, status))), + } + } + }, + ); + Ok(RpcReply::Stream(stream.boxed())) + } + } + } +} + #[async_trait] impl RpcService for EngineRpc { async fn handle(&self, method: &str, params: serde_json::Value) -> Result { @@ -2198,6 +2312,155 @@ impl RpcService for EngineRpc { .map_err(|e| RpcError::Failed(e.to_string()))?; RpcReply::value(&serde_json::json!({ "text": text })) } + // ── encrypted-sync vault ──────────────────────────────────── + methods::VAULT_STATUS => { + Ok(RpcReply::Stream(watch_stream(self.vault()?.watch_status()))) + } + methods::VAULT_REFRESH => { + let status = self + .vault()? + .refresh() + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + let mut value = + serde_json::to_value(status).map_err(|e| RpcError::Failed(e.to_string()))?; + value["migration"] = serde_json::json!(self.doc_host.history_migration_status()); + value["deviceNames"] = serde_json::json!(self.workspace.vault_device_names()); + RpcReply::value(&value) + } + methods::VAULT_RENAME_DEVICE => { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Params { + device_id: String, + name: String, + } + let p: Params = parse_params(params)?; + if !self.vault()?.is_ready() { + return Err(RpcError::Failed( + "Approve this device before renaming devices".into(), + )); + } + let status = self.vault()?.status(); + if !status.devices.iter().any(|d| d.device_id == p.device_id) { + return Err(RpcError::Failed("Device is not part of this vault".into())); + } + self.workspace + .rename_vault_device(&p.device_id, &p.name) + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({"ok": true})) + } + methods::VAULT_CONFIRM_RECOVERY => { + self.vault()? + .confirm_recovery_kit() + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + self.doc_host.start_history_migration(); + RpcReply::value(&serde_json::json!({ "ok": true })) + } + methods::VAULT_MIGRATE_HISTORY => { + self.doc_host.start_history_migration(); + RpcReply::value(&self.doc_host.history_migration_status()) + } + methods::VAULT_SETUP => { + let preparation = self + .doc_host + .prepare_encryption() + .map_err(|e| RpcError::Failed(e.to_string()))?; + let result = self.vault()?.setup().await; + drop(preparation); + let kit = result.map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&kit) + } + methods::VAULT_REQUEST_ENROLLMENT => { + let (request_id, pairing_code) = self + .vault()? + .request_enrollment() + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ + "requestId": request_id, + "pairingCode": pairing_code, + })) + } + methods::VAULT_CANCEL_ENROLLMENT => { + self.vault()? + .cancel_enrollment() + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ "ok": true })) + } + methods::VAULT_PENDING_REQUESTS => { + let requests = self + .vault()? + .pending_requests() + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ "requests": requests })) + } + methods::VAULT_APPROVE => { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct P { + request_id: String, + code: String, + } + let p: P = parse_params(params)?; + self.vault()? + .approve(&p.request_id, &p.code) + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ "ok": true })) + } + methods::VAULT_REJECT => { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct P { + request_id: String, + } + let p: P = parse_params(params)?; + self.vault()? + .reject(&p.request_id) + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ "ok": true })) + } + methods::VAULT_REVOKE => { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct P { + device_id: String, + } + let p: P = parse_params(params)?; + self.vault()? + .revoke(&p.device_id) + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ "ok": true })) + } + methods::VAULT_RECOVER => { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct P { + kit: String, + #[serde(default)] + genesis_hash: Option, + } + let p: P = parse_params(params)?; + let genesis = match p.genesis_hash { + Some(hex) => Some( + crate::vault::store::Hex(hex) + .decode::<32>() + .ok_or_else(|| RpcError::BadParams("genesisHash".into()))?, + ), + None => None, + }; + self.vault()? + .recover(&p.kit, genesis) + .await + .map_err(|e| RpcError::Failed(e.to_string()))?; + RpcReply::value(&serde_json::json!({ "ok": true })) + } other => Err(RpcError::UnknownMethod(other.to_string())), } } @@ -2207,6 +2470,38 @@ impl RpcService for EngineRpc { mod tests { use super::*; + struct RelayProbe; + + #[async_trait] + impl RpcService for RelayProbe { + async fn handle(&self, _: &str, params: serde_json::Value) -> Result { + Ok(RpcReply::Value(params)) + } + } + + #[tokio::test] + async fn relay_rejects_vault_operations_and_encrypted_profile_content() { + use crate::vault::{MemoryProtection, VaultService, VaultStore}; + let dir = tempfile::tempdir().unwrap(); + let store = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + let vault = VaultService::open(store, None, "org", "user"); + let relay = RelayRpc::new(std::sync::Arc::new(RelayProbe), vault); + for method in [ + methods::VAULT_SETUP, + methods::VAULT_RECOVER, + methods::VAULT_APPROVE, + "VaultFutureMethod", + ] { + assert!(relay.handle(method, serde_json::json!({})).await.is_err()); + } + assert!(relay.handle("Echo", serde_json::json!({})).await.is_ok()); + std::fs::write(dir.path().join("vault.json"), b"unreadable").unwrap(); + let store = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + let vault = VaultService::open(store, None, "org", "user"); + let relay = RelayRpc::new(std::sync::Arc::new(RelayProbe), vault); + assert!(relay.handle("Echo", serde_json::json!({})).await.is_err()); + } + /// The UI's Switch/Forget calls send `{id, accountId, harness}` (+ optional /// `targetDeviceId`); the extra fields must be tolerated, `accountId` wins. #[test] diff --git a/crates/engine/src/vault/client.rs b/crates/engine/src/vault/client.rs new file mode 100644 index 000000000..a6957e247 --- /dev/null +++ b/crates/engine/src/vault/client.rs @@ -0,0 +1,435 @@ +//! HTTP client for the edge vault control plane (`edge/src/vault-room.ts`). +//! Every request carries the profile bearer; every response is treated as +//! untrusted bytes until the service verifies it against local pins. + +use serde::Deserialize; + +use crate::EngineError; +use crate::doc_host::EdgeConfig; + +use super::store::Hex; + +#[derive(Clone)] +pub struct VaultClient { + http: reqwest::Client, + edge: EdgeConfig, + org_id: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Descriptor { + pub vault_id: Hex, + pub generation: Hex, + pub head_seq: i64, + pub head_hash: Hex, + pub genesis_hash: Hex, + pub active_epoch: String, + pub profile_hash: Hex, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MembershipPage { + pub records: Vec, + #[serde(default)] + pub truncated: bool, + pub head_seq: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PostOutcome { + #[serde(default)] + pub ok: bool, + #[serde(default)] + pub error: Option, + #[serde(default)] + pub head_seq: Option, + #[serde(default)] + pub head_hash: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ObjectKeyEntry { + pub epoch: String, + pub record: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnrollmentInfo { + pub request_id: Hex, + pub device_id: Hex, + pub signing_key: Hex, + pub encryption_key: Hex, + pub created_at: i64, + pub expires_at: i64, + pub status: String, + #[serde(default)] + pub membership_seq: Option, +} + +/// Outcome of publishing an object key: the bytes every writer must use. +pub struct ObjectKeyPublished { + pub record: Vec, + pub adopted_existing: bool, +} + +impl VaultClient { + pub fn new(http: reqwest::Client, edge: EdgeConfig, org_id: impl Into) -> Self { + Self { + http, + edge, + org_id: org_id.into(), + } + } + + fn url(&self, path: &str) -> String { + format!( + "{}/vault/{}{}", + self.edge.url.trim_end_matches('/'), + self.org_id, + path + ) + } + + async fn bearer(&self) -> Result { + self.edge + .bearer() + .await + .ok_or_else(|| EngineError::Other("not signed in".into())) + } + + async fn send( + &self, + request: reqwest::RequestBuilder, + ) -> Result { + request + .send() + .await + .map_err(|err| EngineError::Other(format!("the edge is unreachable: {err}"))) + } + + async fn json( + res: reqwest::Response, + ) -> Result { + res.json::() + .await + .map_err(|err| EngineError::Other(format!("malformed vault response: {err}"))) + } + + /// `None` when no vault exists for this profile. + pub async fn descriptor(&self) -> Result, EngineError> { + let res = self + .send( + self.http + .get(self.url("")) + .bearer_auth(self.bearer().await?), + ) + .await?; + match res.status().as_u16() { + 404 => Ok(None), + _ if res.status().is_success() => Ok(Some(Self::json(res).await?)), + code => Err(EngineError::Other(format!("vault descriptor http {code}"))), + } + } + + pub async fn membership_after(&self, after: i64) -> Result { + let res = self + .send( + self.http + .get(self.url("/membership")) + .query(&[("after", after.to_string())]) + .bearer_auth(self.bearer().await?), + ) + .await?; + if !res.status().is_success() { + return Err(EngineError::Other(format!( + "vault membership http {}", + res.status().as_u16() + ))); + } + Self::json(res).await + } + + /// Append a signed policy record. `Ok(Err(outcome))` is a server-side + /// refusal (stale parent, invalid record) the caller may recover from. + pub async fn post_membership( + &self, + record: Vec, + ) -> Result, EngineError> { + let res = self + .send( + self.http + .post(self.url("/membership")) + .bearer_auth(self.bearer().await?) + .header("content-type", "application/octet-stream") + .body(record), + ) + .await?; + let success = res.status().is_success(); + let outcome: PostOutcome = Self::json(res).await?; + Ok(if success && outcome.ok { + Ok(outcome) + } else { + Err(outcome) + }) + } + + pub async fn get_envelope(&self, recipient: &[u8; 16]) -> Result>, EngineError> { + let path = format!("/envelopes/{}", Hex::of(recipient).0); + let res = self + .send( + self.http + .get(self.url(&path)) + .bearer_auth(self.bearer().await?), + ) + .await?; + match res.status().as_u16() { + 404 => Ok(None), + _ if res.status().is_success() => Ok(Some( + res.bytes() + .await + .map_err(|err| EngineError::Other(err.to_string()))? + .to_vec(), + )), + code => Err(EngineError::Other(format!("vault envelope http {code}"))), + } + } + + pub async fn put_envelope( + &self, + recipient: &[u8; 16], + record: Vec, + ) -> Result<(), EngineError> { + let path = format!("/envelopes/{}", Hex::of(recipient).0); + let res = self + .send( + self.http + .put(self.url(&path)) + .bearer_auth(self.bearer().await?) + .header("content-type", "application/octet-stream") + .body(record), + ) + .await?; + if !res.status().is_success() { + let code = res.status().as_u16(); + let body: PostOutcome = Self::json(res).await.unwrap_or(PostOutcome { + ok: false, + error: None, + head_seq: None, + head_hash: None, + }); + return Err(EngineError::Other(format!( + "vault envelope rejected ({code}: {})", + body.error.unwrap_or_default() + ))); + } + Ok(()) + } + + pub async fn object_keys(&self, object: &[u8; 16]) -> Result, EngineError> { + #[derive(Deserialize)] + struct Body { + keys: Vec, + } + let path = format!("/objects/{}/keys", Hex::of(object).0); + let res = self + .send( + self.http + .get(self.url(&path)) + .bearer_auth(self.bearer().await?), + ) + .await?; + if !res.status().is_success() { + return Err(EngineError::Other(format!( + "vault object keys http {}", + res.status().as_u16() + ))); + } + Ok(Self::json::(res).await?.keys) + } + + /// First writer wins: a 409 returns the record already stored for this + /// object/epoch, which the caller must adopt in place of its own. + pub async fn put_object_key( + &self, + object: &[u8; 16], + record: Vec, + ) -> Result { + #[derive(Deserialize)] + struct Body { + #[serde(default)] + conflict: bool, + #[serde(default)] + record: Option, + #[serde(default)] + error: Option, + } + let path = format!("/objects/{}/keys", Hex::of(object).0); + let res = self + .send( + self.http + .put(self.url(&path)) + .bearer_auth(self.bearer().await?) + .header("content-type", "application/octet-stream") + .body(record.clone()), + ) + .await?; + let status = res.status().as_u16(); + let body: Body = Self::json(res).await?; + if status == 409 && body.conflict { + let stored = body + .record + .as_deref() + .and_then(decode_base64) + .ok_or_else(|| EngineError::Other("vault key conflict without record".into()))?; + return Ok(ObjectKeyPublished { + record: stored, + adopted_existing: true, + }); + } + if !(200..300).contains(&status) { + return Err(EngineError::Other(format!( + "vault object key rejected ({status}: {})", + body.error.unwrap_or_default() + ))); + } + Ok(ObjectKeyPublished { + record, + adopted_existing: false, + }) + } + + pub async fn create_enrollment( + &self, + request: &zeron_crypto::policy::EnrollmentRequest, + proof: &[u8; 64], + ) -> Result { + let body = serde_json::json!({ + "requestId": Hex::of(&request.request_id).0, + "deviceId": Hex::of(&request.device_id).0, + "signingKey": Hex::of(&request.signing_key).0, + "encryptionKey": Hex::of(&request.encryption_key).0, + "proof": Hex::of(proof).0, + }); + let res = self + .send( + self.http + .post(self.url("/enroll")) + .bearer_auth(self.bearer().await?) + .json(&body), + ) + .await?; + if !res.status().is_success() { + let code = res.status().as_u16(); + let body: PostOutcome = Self::json(res).await.unwrap_or(PostOutcome { + ok: false, + error: None, + head_seq: None, + head_hash: None, + }); + return Err(EngineError::Other(format!( + "enrollment refused ({code}: {})", + body.error.unwrap_or_default() + ))); + } + Self::json(res).await + } + + pub async fn list_enrollments(&self) -> Result, EngineError> { + #[derive(Deserialize)] + struct Body { + requests: Vec, + } + let res = self + .send( + self.http + .get(self.url("/enroll")) + .bearer_auth(self.bearer().await?), + ) + .await?; + if res.status().as_u16() == 404 { + return Ok(Vec::new()); + } + if !res.status().is_success() { + return Err(EngineError::Other(format!( + "enrollment list http {}", + res.status().as_u16() + ))); + } + Ok(Self::json::(res).await?.requests) + } + + pub async fn enrollment( + &self, + request_id: &[u8; 16], + ) -> Result, EngineError> { + let path = format!("/enroll/{}", Hex::of(request_id).0); + let res = self + .send( + self.http + .get(self.url(&path)) + .bearer_auth(self.bearer().await?), + ) + .await?; + match res.status().as_u16() { + 404 => Ok(None), + _ if res.status().is_success() => Ok(Some(Self::json(res).await?)), + code => Err(EngineError::Other(format!("enrollment status http {code}"))), + } + } + + pub async fn approve_enrollment( + &self, + request_id: &[u8; 16], + membership_seq: i64, + ) -> Result<(), EngineError> { + let path = format!("/enroll/{}/approve", Hex::of(request_id).0); + let res = self + .send( + self.http + .post(self.url(&path)) + .bearer_auth(self.bearer().await?) + .json(&serde_json::json!({ "membershipSeq": membership_seq })), + ) + .await?; + if !res.status().is_success() { + return Err(EngineError::Other(format!( + "enrollment approve http {}", + res.status().as_u16() + ))); + } + Ok(()) + } + + pub async fn reject_enrollment(&self, request_id: &[u8; 16]) -> Result<(), EngineError> { + let path = format!("/enroll/{}/reject", Hex::of(request_id).0); + let res = self + .send( + self.http + .post(self.url(&path)) + .bearer_auth(self.bearer().await?), + ) + .await?; + if !res.status().is_success() && res.status().as_u16() != 404 { + return Err(EngineError::Other(format!( + "enrollment reject http {}", + res.status().as_u16() + ))); + } + Ok(()) + } +} + +pub fn decode_base64(text: &str) -> Option> { + use base64::Engine as _; + base64::engine::general_purpose::STANDARD.decode(text).ok() +} + +pub fn encode_base64(bytes: &[u8]) -> String { + use base64::Engine as _; + base64::engine::general_purpose::STANDARD.encode(bytes) +} diff --git a/crates/engine/src/vault/mod.rs b/crates/engine/src/vault/mod.rs new file mode 100644 index 000000000..d44f55375 --- /dev/null +++ b/crates/engine/src/vault/mod.rs @@ -0,0 +1,22 @@ +//! Per-profile encrypted-sync vault (RFC 0001): device identity and key +//! persistence (`store`), the edge control-plane client (`client`), and the +//! lifecycle service that verifies membership, holds keys, and hands sealing +//! / opening material to the content transports (`service`). +//! +//! Nothing here activates encrypted transport by itself: the service exposes +//! explicit states and the transports consult it before serializing content. + +pub mod client; +pub mod registry_codec; +pub mod service; +pub mod store; + +pub use registry_codec::VaultRegistryCodec; +pub use service::{ + ChatKeyMaterial, OpenContext, OpenFailure, RecoveryKit, VaultDevice, VaultPhase, VaultService, + VaultStatus, object_id_for, +}; +pub use store::{ + LockedProtection, MemoryProtection, ProtectionKeyProvider, ProtectionMode, VaultStore, + VaultStoreError, platform_protection, +}; diff --git a/crates/engine/src/vault/registry_codec.rs b/crates/engine/src/vault/registry_codec.rs new file mode 100644 index 000000000..0fa31cc28 --- /dev/null +++ b/crates/engine/src/vault/registry_codec.rs @@ -0,0 +1,258 @@ +//! Registry field encryption (RFC 0001 §9): each field VALUE becomes a +//! content record (purpose RegistryField) whose authenticated plaintext also +//! names the row kind, row id, field, and original clock, so the server can +//! keep merging by row/field/HLC while a relay can neither read a value nor +//! move one between slots. Every field of the profile's registry shares one +//! object (`registry`) so the control plane holds one key per epoch, not +//! one per field. +//! +//! Wire value: `{"e1": ""}`. A plaintext value where a +//! sealed one is required is REJECTED (never displayed): an encrypted profile +//! reads only its encrypted registry generation. + +use std::sync::{Arc, Mutex}; + +use serde::{Deserialize, Serialize}; +use zeron_crypto::content::{self, ContentPurpose}; +use zeron_crypto::record::UnverifiedRecord; +use zeron_sync::{FieldOpenFailure, RegistryCodec}; + +use super::{ChatKeyMaterial, OpenFailure, VaultService, object_id_for}; +use crate::EngineError; + +/// Registry field plaintext cap: the edge's 16 KiB per-op budget minus the +/// record overhead and base64 expansion, spread over a row's fields. +const MAX_FIELD_PLAINTEXT: usize = 8 * 1024; +/// A lifecycle proof names a row and a clock — small by construction. +const MAX_LIFECYCLE_PLAINTEXT: usize = 1024; +const WIRE_KEY: &str = "e1"; +const LIFECYCLE_DELETE: &str = "delete"; + +#[derive(Serialize, Deserialize)] +struct FieldPlaintext<'a> { + kind: &'a str, + id: &'a str, + field: &'a str, + hlc: &'a str, + value: serde_json::Value, +} + +/// Row lifecycle proof (purpose RegistryLifecycle): a member's signed, +/// sealed statement that row `kind/id` was deleted at tombstone clock `hlc`. +#[derive(Serialize, Deserialize)] +struct LifecyclePlaintext<'a> { + kind: &'a str, + id: &'a str, + op: &'a str, + hlc: &'a str, +} + +pub struct VaultRegistryCodec { + vault: VaultService, + object_id: [u8; 16], + material: Mutex>>, +} + +impl VaultRegistryCodec { + pub fn new(vault: VaultService, user_id: &str) -> Self { + Self { + vault, + object_id: object_id_for("registry", user_id), + material: Mutex::new(None), + } + } + + pub fn vault(&self) -> &VaultService { + &self.vault + } + + /// Obtain (or refresh after an epoch change) the sealing material. The + /// object key becomes durable on the control plane before this returns. + pub async fn prepare(&self) -> Result<(), EngineError> { + let material = self.vault.seal_material(self.object_id).await?; + *lock(&self.material) = Some(Arc::new(material)); + Ok(()) + } + + /// Drop cached material (vault left Ready): sealing pauses until the + /// next successful `prepare`. + pub fn clear(&self) { + *lock(&self.material) = None; + } +} + +fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { + mutex + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +impl RegistryCodec for VaultRegistryCodec { + fn seal_field( + &self, + kind: &str, + id: &str, + field: &str, + hlc: &str, + value: &serde_json::Value, + ) -> Result { + let material = lock(&self.material) + .clone() + .ok_or_else(|| "vault keys not ready".to_string())?; + // The sealing binding must still be the head: a stale head would + // author under a superseded revision (readers accept it as history, + // but a rotation must move new writes forward). + if self.vault.current_content_binding(self.object_id) != Some(material.binding) { + return Err("vault epoch changed; re-preparing".into()); + } + let plaintext = serde_json::to_vec(&FieldPlaintext { + kind, + id, + field, + hlc, + value: value.clone(), + }) + .map_err(|e| e.to_string())?; + if plaintext.len() > MAX_FIELD_PLAINTEXT { + return Err(format!("registry field {field} exceeds the sealed budget")); + } + let sealed = content::seal( + &material.binding, + ContentPurpose::RegistryField, + &material.key, + &material.signer, + &plaintext, + MAX_FIELD_PLAINTEXT, + ) + .map_err(|e| e.to_string())?; + Ok(serde_json::json!({ WIRE_KEY: super::client::encode_base64(sealed.encoded()) })) + } + + fn open_field( + &self, + kind: &str, + id: &str, + field: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result, FieldOpenFailure> { + let Some(encoded) = wire + .get(WIRE_KEY) + .and_then(|v| v.as_str()) + .and_then(super::client::decode_base64) + else { + return Err(FieldOpenFailure::Rejected); + }; + let parsed = UnverifiedRecord::parse(&encoded, MAX_FIELD_PLAINTEXT + 144) + .map_err(|_| FieldOpenFailure::Rejected)?; + let binding = *parsed.untrusted_binding(); + let context = self + .vault + .open_material_cached(self.object_id, &binding) + .map_err(|failure| match failure { + OpenFailure::Unavailable | OpenFailure::KeyUnavailable => { + self.vault.spawn_key_refresh(self.object_id); + FieldOpenFailure::KeyUnavailable + } + OpenFailure::NotAuthorized => FieldOpenFailure::Rejected, + })?; + let opened = content::open( + &encoded, + &context.binding, + ContentPurpose::RegistryField, + &context.key, + &context.author_public_key, + MAX_FIELD_PLAINTEXT, + ) + .map_err(|_| FieldOpenFailure::Rejected)?; + let plaintext: FieldPlaintext = serde_json::from_slice(opened.plaintext().as_bytes()) + .map_err(|_| FieldOpenFailure::Rejected)?; + // The authenticated slot must be the slot the server filed it under. + if plaintext.kind != kind + || plaintext.id != id + || plaintext.field != field + || plaintext.hlc != hlc + { + return Err(FieldOpenFailure::Rejected); + } + Ok(if plaintext.value.is_null() { + None + } else { + Some(plaintext.value) + }) + } + + fn seal_lifecycle(&self, kind: &str, id: &str, hlc: &str) -> Result { + let material = lock(&self.material) + .clone() + .ok_or_else(|| "vault keys not ready".to_string())?; + if self.vault.current_content_binding(self.object_id) != Some(material.binding) { + return Err("vault epoch changed; re-preparing".into()); + } + let plaintext = serde_json::to_vec(&LifecyclePlaintext { + kind, + id, + op: LIFECYCLE_DELETE, + hlc, + }) + .map_err(|e| e.to_string())?; + let sealed = content::seal( + &material.binding, + ContentPurpose::RegistryLifecycle, + &material.key, + &material.signer, + &plaintext, + MAX_LIFECYCLE_PLAINTEXT, + ) + .map_err(|e| e.to_string())?; + Ok(serde_json::json!({ WIRE_KEY: super::client::encode_base64(sealed.encoded()) })) + } + + fn open_lifecycle( + &self, + kind: &str, + id: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result<(), FieldOpenFailure> { + let Some(encoded) = wire + .get(WIRE_KEY) + .and_then(|v| v.as_str()) + .and_then(super::client::decode_base64) + else { + return Err(FieldOpenFailure::Rejected); + }; + let parsed = UnverifiedRecord::parse(&encoded, MAX_LIFECYCLE_PLAINTEXT + 144) + .map_err(|_| FieldOpenFailure::Rejected)?; + let binding = *parsed.untrusted_binding(); + let context = self + .vault + .open_material_cached(self.object_id, &binding) + .map_err(|failure| match failure { + OpenFailure::Unavailable | OpenFailure::KeyUnavailable => { + self.vault.spawn_key_refresh(self.object_id); + FieldOpenFailure::KeyUnavailable + } + OpenFailure::NotAuthorized => FieldOpenFailure::Rejected, + })?; + let opened = content::open( + &encoded, + &context.binding, + ContentPurpose::RegistryLifecycle, + &context.key, + &context.author_public_key, + MAX_LIFECYCLE_PLAINTEXT, + ) + .map_err(|_| FieldOpenFailure::Rejected)?; + let plaintext: LifecyclePlaintext = serde_json::from_slice(opened.plaintext().as_bytes()) + .map_err(|_| FieldOpenFailure::Rejected)?; + if plaintext.kind != kind + || plaintext.id != id + || plaintext.op != LIFECYCLE_DELETE + || plaintext.hlc != hlc + { + return Err(FieldOpenFailure::Rejected); + } + Ok(()) + } +} diff --git a/crates/engine/src/vault/service.rs b/crates/engine/src/vault/service.rs new file mode 100644 index 000000000..24980d5b7 --- /dev/null +++ b/crates/engine/src/vault/service.rs @@ -0,0 +1,2151 @@ +//! The vault lifecycle (RFC 0001 §4.3, §5, §6, §11): explicit states, the +//! verified membership history this device pins, its keyring, and the key +//! material it hands to content transports. +//! +//! Trust flows only from local pins: a genesis this device created, a +//! pairing the user confirmed by comparison code, or a recovery kit the user +//! typed. Server responses are verified against those pins; a server can +//! withhold data (availability) but cannot substitute trust. + +use std::collections::BTreeMap; +use std::sync::{Arc, Mutex}; + +use serde::Serialize; +use tokio::sync::watch; +use zeron_crypto::content::{ContentKey, DeviceSigner, KeyScope}; +use zeron_crypto::envelope::{self, RecipientKind}; +use zeron_crypto::hpke::{HpkePrivateKey, HpkePublicKey}; +use zeron_crypto::keyring::Keyring; +use zeron_crypto::policy::{ + self, DeviceStatus, EnrollmentRequest, MembershipState, Operation, POLICY_OBJECT_ID, +}; +use zeron_crypto::record::{RecordBinding, RecordKind, UnverifiedRecord}; +use zeron_crypto::recovery::RecoverySecret; + +use super::client::{VaultClient, decode_base64, encode_base64}; +use super::store::{ + CachedObjectKey, DeviceIdentity, Hex, LocalVaultState, PendingEnrollment, PinnedVault, + ProtectionMode, Secret, VaultStore, VaultStoreError, +}; +use crate::EngineError; + +const OBJECT_ID_DOMAIN: &[u8] = b"zeron/object-id/v1\0"; +const MAX_MEMBERSHIP_RECORDS: usize = 4096; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(tag = "phase", rename_all = "camelCase")] +pub enum VaultPhase { + /// Local-only profile or no edge: encryption does not apply. + Unavailable { + reason: String, + }, + /// Secure key storage could not be opened; keys are preserved. + Locked { + reason: String, + }, + /// This device has no membership. `remote_vault` says whether one + /// already exists for the profile (approve/recover) or not (set up). + NotEnrolled { + remote_vault: bool, + }, + /// An enrollment request awaits approval on another device. + Pending { + request_id: String, + pairing_code: String, + expires_at: i64, + }, + Ready, + RecoveryConfirmationRequired, + /// Membership advanced to an epoch whose key this device does not hold. + KeyUpdateRequired, + /// Server data failed verification against local pins. + VerificationFailed { + reason: String, + }, + Revoked, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultDevice { + pub device_id: String, + pub status: String, + pub this_device: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PendingRequest { + pub request_id: String, + pub device_id: String, + pub pairing_code: String, + pub expires_at: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VaultStatus { + #[serde(flatten)] + pub phase: VaultPhase, + pub vault_id: Option, + pub genesis_hash: Option, + pub device_id: Option, + pub epoch: Option, + pub devices: Vec, + pub protection: ProtectionMode, +} + +/// The recovery kit shown once at setup / rotation (RFC §4.1). The kit text +/// is the secret; the file carries only public trust-anchor metadata. +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RecoveryKit { + pub kit: String, + pub recovery_file: serde_json::Value, +} + +impl std::fmt::Debug for RecoveryKit { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("RecoveryKit([REDACTED])") + } +} + +/// Everything a writer needs to seal one record for an object under the +/// current epoch. +pub struct ChatKeyMaterial { + pub binding: RecordBinding, + pub key: Arc, + pub signer: Arc, +} + +/// Everything a reader needs to open one record whose untrusted binding it +/// has parsed: the TRUSTED binding rebuilt from pinned history, the key, and +/// the author's public key at that revision. +pub struct OpenContext { + pub binding: RecordBinding, + pub key: Arc, + pub author_public_key: [u8; 32], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpenFailure { + /// Vault not ready on this device (locked, not enrolled, revoked). + Unavailable, + /// The record names an epoch / object key this device does not hold yet. + KeyUnavailable, + /// The record's membership hash / author / epoch are not in trusted history. + NotAuthorized, +} + +#[derive(Clone)] +struct Revision { + hash: [u8; 32], + state: MembershipState, +} + +struct Trust { + device_id: [u8; 16], + signer: Arc, + encryption: HpkePrivateKey, + history: Vec, + keyring: Keyring, + object_keys: BTreeMap<([u8; 16], u64), Arc>, +} + +impl Trust { + fn head(&self) -> &MembershipState { + &self.history.last().expect("non-empty history").state + } + + fn revision(&self, hash: &[u8; 32]) -> Option<&Revision> { + self.history.iter().rev().find(|r| r.hash == *hash) + } + + fn signing_key(&self) -> [u8; 32] { + self.signer.public_key().try_into().expect("32-byte key") + } +} + +struct Guarded { + encryption_required: bool, + state: LocalVaultState, + trust: Option, + locked: Option, + verification_failure: Option, + remote_vault: Option, +} + +impl Guarded { + fn content_blocked(&self) -> bool { + self.locked.is_some() + || self.verification_failure.is_some() + || self.state.pending_membership.is_some() + || self.state.setup_recovery.is_some() + || !self.state.owed_envelopes.is_empty() + } +} + +struct Inner { + store: VaultStore, + client: Option, + profile_hash: [u8; 32], + guarded: Mutex, + /// Serializes network-mutating operations (setup/approve/revoke/refresh). + ops: tokio::sync::Mutex<()>, + status: watch::Sender, +} + +#[derive(Clone)] +pub struct VaultService { + inner: Arc, +} + +impl std::fmt::Debug for VaultService { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "VaultService({:?})", self.status().phase) + } +} + +fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { + mutex + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// (trusted binding, author public key, epoch key held, cached object key). +type TrustedContext = (RecordBinding, [u8; 32], bool, Option>); + +/// Opaque 16-byte object id for a chat/registry/blob identifier. +pub fn object_id_for(kind: &str, id: &str) -> [u8; 16] { + let digest = zeron_crypto::sha256(&[OBJECT_ID_DOMAIN, kind.as_bytes(), b"\0", id.as_bytes()]); + let mut out = [0; 16]; + out.copy_from_slice(&digest[..16]); + out +} + +impl VaultService { + /// Open the local store and rebuild trust. Never panics on a locked or + /// corrupt store: the status says so and the file is left untouched. + pub fn open( + store: VaultStore, + client: Option, + org_id: &str, + user_id: &str, + ) -> Self { + let profile_hash = policy::profile_hash(org_id, user_id); + let (state, locked) = match store.load() { + Ok(state) => (state, None), + Err(VaultStoreError::Locked(reason)) => (LocalVaultState::default(), Some(reason)), + Err(err) => (LocalVaultState::default(), Some(err.to_string())), + }; + let mut guarded = Guarded { + encryption_required: store.exists(), + state, + trust: None, + locked, + verification_failure: None, + remote_vault: None, + }; + if guarded.locked.is_none() { + match rebuild_trust(&guarded.state, &profile_hash) { + Ok(trust) => guarded.trust = trust, + Err(reason) => guarded.verification_failure = Some(reason), + } + } + let (status, _) = watch::channel(VaultStatus { + phase: VaultPhase::Unavailable { + reason: "starting".into(), + }, + vault_id: None, + genesis_hash: None, + device_id: None, + epoch: None, + devices: Vec::new(), + protection: store.mode(), + }); + let service = Self { + inner: Arc::new(Inner { + store, + client, + profile_hash, + guarded: Mutex::new(guarded), + ops: tokio::sync::Mutex::new(()), + status, + }), + }; + service.publish_status(); + service + } + + pub fn status(&self) -> VaultStatus { + self.inner.status.borrow().clone() + } + + pub fn watch_status(&self) -> watch::Receiver { + self.inner.status.subscribe() + } + + /// True when content transports must seal/open (this device is an + /// approved member with a usable keyring). + pub fn is_ready(&self) -> bool { + matches!(self.status().phase, VaultPhase::Ready) + } + + /// True when the profile is enrolled in a vault at all: a member whose + /// keys are stale, locked, or revoked must still never write plaintext. + pub fn is_enrolled(&self) -> bool { + let guarded = lock(&self.inner.guarded); + guarded.encryption_required + || guarded.state.vault.is_some() + || guarded.remote_vault == Some(true) + } + + pub fn device_id(&self) -> Option<[u8; 16]> { + lock(&self.inner.guarded) + .trust + .as_ref() + .map(|t| t.device_id) + } + + /// This device's identity for the authenticated device channel (RFC + /// 0001 §10): its vault device id, X25519 static, and the vault scope + /// at the pinned membership head. Refused unless the head is trusted + /// and this device is active in it. + fn channel_local(&self) -> Result { + let guarded = lock(&self.inner.guarded); + if let Some(reason) = &guarded.locked { + return Err(format!("vault locked: {reason}")); + } + if let Some(reason) = &guarded.verification_failure { + return Err(format!("vault verification failed: {reason}")); + } + let trust = guarded + .trust + .as_ref() + .ok_or("device is not an approved vault member")?; + let head = trust.head(); + head.active_device(&trust.device_id) + .ok_or("device is not an active vault member")?; + let secret = guarded + .state + .device + .as_ref() + .and_then(|device| device.encryption_secret.decode::<32>()) + .ok_or("device encryption secret unavailable")?; + let identity = zeron_crypto::channel::ChannelIdentity::new(trust.device_id, &secret) + .map_err(|e| e.to_string())?; + Ok(zeron_rpc::ChannelLocal { + identity, + scope: zeron_crypto::channel::ChannelScope { + vault_id: *head.vault_id(), + generation: *head.generation(), + }, + }) + } + + /// The channel membership check: an ACTIVE device at the pinned head + /// whose published encryption key is the peer's Noise static, and which + /// is not this device. A clone presenting our own identity is refused. + fn channel_accepts(&self, peer: &zeron_crypto::channel::PeerIdentity) -> bool { + let guarded = lock(&self.inner.guarded); + if guarded.locked.is_some() || guarded.verification_failure.is_some() { + return false; + } + let Some(trust) = guarded.trust.as_ref() else { + return false; + }; + if peer.device_id == trust.device_id { + return false; + } + trust + .head() + .active_device(&peer.device_id) + .is_some_and(|device| device.encryption_key == peer.static_key) + } + + fn publish_status(&self) { + let status = { + let guarded = lock(&self.inner.guarded); + self.compute_status(&guarded) + }; + self.inner.status.send_replace(status); + } + + fn compute_status(&self, guarded: &Guarded) -> VaultStatus { + let protection = self.inner.store.mode(); + let device_id = guarded + .state + .device + .as_ref() + .map(|d| d.device_id.0.clone()) + .or_else(|| guarded.trust.as_ref().map(|t| Hex::of(&t.device_id).0)); + let base = |phase: VaultPhase| VaultStatus { + phase, + vault_id: guarded.state.vault.as_ref().map(|v| v.vault_id.0.clone()), + genesis_hash: guarded + .trust + .as_ref() + .map(|t| Hex::of(t.head().genesis_hash()).0), + device_id: device_id.clone(), + epoch: None, + devices: Vec::new(), + protection, + }; + if self.inner.client.is_none() { + return base(VaultPhase::Unavailable { + reason: "this profile does not sync".into(), + }); + } + if let Some(reason) = &guarded.locked { + return base(VaultPhase::Locked { + reason: reason.clone(), + }); + } + if let Some(reason) = &guarded.verification_failure { + return base(VaultPhase::VerificationFailed { + reason: reason.clone(), + }); + } + if guarded.state.pending_membership.is_some() { + return base(VaultPhase::KeyUpdateRequired); + } + let Some(trust) = &guarded.trust else { + if let Some(pending) = &guarded.state.enrollment { + let code = guarded + .state + .vault + .as_ref() + .and_then(|v| v.membership.first()) + .and_then(|g| decode_base64(g)) + .map(|g| policy::membership_hash(&g)) + .and_then(|genesis| { + pending_request(&guarded.state, &pending.request_id) + .map(|r| r.pairing_code(&genesis)) + }) + .unwrap_or_default(); + return base(VaultPhase::Pending { + request_id: pending.request_id.0.clone(), + pairing_code: code, + expires_at: pending.created_at + 15 * 60 * 1000, + }); + } + return base(VaultPhase::NotEnrolled { + remote_vault: guarded.remote_vault.unwrap_or(false), + }); + }; + let head = trust.head(); + let devices = head + .devices() + .iter() + .map(|d| VaultDevice { + device_id: Hex::of(&d.device_id).0, + status: match d.status { + DeviceStatus::Active => "active".into(), + DeviceStatus::Revoked => "revoked".into(), + }, + this_device: d.device_id == trust.device_id, + }) + .collect(); + let phase = if head.active_device(&trust.device_id).is_none() { + VaultPhase::Revoked + } else if trust.keyring.epoch_key(head.epoch()).is_none() + || !guarded.state.owed_envelopes.is_empty() + { + VaultPhase::KeyUpdateRequired + } else if guarded.state.setup_recovery.is_some() { + VaultPhase::RecoveryConfirmationRequired + } else { + VaultPhase::Ready + }; + VaultStatus { + phase, + vault_id: Some(Hex::of(head.vault_id()).0), + genesis_hash: Some(Hex::of(head.genesis_hash()).0), + device_id, + epoch: Some(head.epoch()), + devices, + protection, + } + } + + fn client(&self) -> Result<&VaultClient, EngineError> { + self.inner + .client + .as_ref() + .ok_or_else(|| EngineError::Other("this profile does not sync".into())) + } + + fn commit(&self, guarded: &mut Guarded) -> Result<(), EngineError> { + if let Err(error) = self.inner.store.save(&guarded.state) { + guarded.encryption_required = true; + guarded.locked = Some(error.to_string()); + self.inner.status.send_replace(self.compute_status(guarded)); + return Err(error.into()); + } + self.inner.status.send_replace(self.compute_status(guarded)); + Ok(()) + } + + async fn finish_pending_membership(&self, client: &VaultClient) -> Result<(), EngineError> { + let pending = { + let guarded = lock(&self.inner.guarded); + if guarded.state.pending_membership.is_none() + && guarded.state.owed_envelopes.is_empty() + && guarded.state.pending_approval.is_none() + { + return Ok(()); + } + guarded.state.pending_membership.clone() + }; + if let Some(encoded) = pending { + let (record, sequence) = { + let guarded = lock(&self.inner.guarded); + if guarded.locked.is_some() || guarded.verification_failure.is_some() { + return Err(EngineError::Other("vault is locked or unverified".into())); + } + let candidate = rebuild_trust(&guarded.state, &self.inner.profile_hash) + .map_err(EngineError::Other)? + .ok_or_else(|| { + EngineError::Other("pending membership has no trust anchor".into()) + })?; + let record = decode_base64(&encoded) + .ok_or_else(|| EngineError::Other("invalid pending membership".into()))?; + if policy::membership_hash(&record) != *candidate.head().hash() { + return Err(EngineError::Other( + "pending membership does not match the journal".into(), + )); + } + (record, candidate.head().sequence()) + }; + if !matches!(client.post_membership(record).await, Ok(Ok(_))) { + let after = i64::try_from(sequence) + .map_err(|_| EngineError::Other("membership sequence overflow".into()))? + - 1; + let page = client.membership_after(after).await?; + if page.records.first() != Some(&encoded) { + return Err(EngineError::Other( + "pending membership was not accepted; journal retained".into(), + )); + } + } + let mut guarded = lock(&self.inner.guarded); + guarded.trust = rebuild_trust(&guarded.state, &self.inner.profile_hash) + .map_err(EngineError::Other)?; + guarded.state.pending_membership = None; + self.commit(&mut guarded)?; + } + self.pull_membership(client).await?; + self.pull_keyring(client).await?; + self.publish_owed(client).await?; + let approval = lock(&self.inner.guarded).state.pending_approval.clone(); + if !lock(&self.inner.guarded).state.owed_envelopes.is_empty() { + return Err(EngineError::Other( + "key envelopes are still pending; refresh to retry".into(), + )); + } + if let Some((request, sequence)) = approval { + let request = request + .decode::<16>() + .ok_or_else(|| EngineError::Other("invalid pending approval".into()))?; + client.approve_enrollment(&request, sequence).await?; + let mut guarded = lock(&self.inner.guarded); + guarded.state.pending_approval = None; + self.commit(&mut guarded)?; + } + self.publish_status(); + Ok(()) + } + + fn setup_kit(&self) -> Result { + let guarded = lock(&self.inner.guarded); + if guarded.locked.is_some() || guarded.verification_failure.is_some() { + return Err(EngineError::Other("vault is locked or unverified".into())); + } + let secret = guarded + .state + .setup_recovery + .as_ref() + .and_then(Secret::bytes) + .ok_or_else(|| EngineError::Other("no recovery kit awaits confirmation".into()))?; + let recovery = + RecoverySecret::from_bytes(&secret).map_err(|e| EngineError::Other(e.to_string()))?; + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("vault setup is pending".into()))?; + Ok(RecoveryKit { + kit: recovery.to_kit().to_string(), + recovery_file: recovery_file(trust.head()), + }) + } + + pub async fn confirm_recovery_kit(&self) -> Result<(), EngineError> { + let _ops = self.inner.ops.lock().await; + if matches!(self.status().phase, VaultPhase::Ready) { + return Ok(()); + } + if !matches!( + self.status().phase, + VaultPhase::RecoveryConfirmationRequired + ) { + return Err(EngineError::Other( + "recovery kit is not ready for confirmation".into(), + )); + } + let mut guarded = lock(&self.inner.guarded); + guarded.state.setup_recovery = None; + self.commit(&mut guarded) + } + + /// Ensure this device has a private identity (generated once, persisted + /// before any use so a crash cannot produce two identities). + fn ensure_identity(&self, guarded: &mut Guarded) -> Result { + if let Some(reason) = &guarded.locked { + return Err(EngineError::Other(format!("vault locked: {reason}"))); + } + if guarded.state.device.is_none() { + let mut device_id = [0u8; 16]; + let mut seed = [0u8; 32]; + zeron_crypto::fill_random(&mut device_id) + .map_err(|e| EngineError::Other(e.to_string()))?; + zeron_crypto::fill_random(&mut seed).map_err(|e| EngineError::Other(e.to_string()))?; + let encryption = + HpkePrivateKey::generate().map_err(|e| EngineError::Other(e.to_string()))?; + guarded.state.device = Some(DeviceIdentity { + device_id: Hex::of(&device_id), + signing_seed: Secret::of(&seed), + encryption_secret: Secret::of(encryption.expose_secret()), + }); + guarded.state.version = 1; + self.commit(guarded)?; + } + identity_of(&guarded.state) + } + + // ── setup ──────────────────────────────────────────────────────────────── + + /// Create the vault for this profile with this device as its first + /// member. Fails if a vault already exists (approve or recover instead). + pub async fn setup(&self) -> Result { + let _ops = self.inner.ops.lock().await; + let client = self.client()?.clone(); + let resuming = lock(&self.inner.guarded).state.setup_recovery.is_some(); + if resuming { + self.finish_pending_membership(&client).await?; + return self.setup_kit(); + } + if lock(&self.inner.guarded).trust.is_some() { + return Err(EngineError::Other("this device is already enrolled".into())); + } + if client.descriptor().await?.is_some() { + lock(&self.inner.guarded).remote_vault = Some(true); + self.publish_status(); + return Err(EngineError::Other( + "a vault already exists for this account; approve this device from another device or use your recovery key" + .into(), + )); + } + let identity = { + let mut guarded = lock(&self.inner.guarded); + self.ensure_identity(&mut guarded)? + }; + let recovery = RecoverySecret::generate().map_err(|e| EngineError::Other(e.to_string()))?; + let recovery_signer = recovery + .signer() + .map_err(|e| EngineError::Other(e.to_string()))?; + let recovery_encryption = recovery + .encryption_key() + .map_err(|e| EngineError::Other(e.to_string()))?; + let mut vault_id = [0u8; 16]; + let mut generation = [0u8; 16]; + zeron_crypto::fill_random(&mut vault_id).map_err(|e| EngineError::Other(e.to_string()))?; + zeron_crypto::fill_random(&mut generation) + .map_err(|e| EngineError::Other(e.to_string()))?; + let payload = policy::PolicyPayload { + sequence: 0, + parent_hash: [0; 32], + profile_hash: self.inner.profile_hash, + epoch: 1, + operation: Operation::Genesis, + recovery_signing_key: recovery_signer + .public_key() + .try_into() + .map_err(|_| EngineError::Other("recovery key".into()))?, + recovery_encryption_key: *recovery_encryption.public_key().as_bytes(), + devices: vec![identity.entry()], + }; + let genesis = policy::encode_policy( + &policy::policy_binding(vault_id, generation, 1, identity.device_id, [0; 32]), + &payload, + &identity.signer, + ) + .map_err(|e| EngineError::Other(format!("genesis: {e}")))?; + let state = MembershipState::from_genesis( + &genesis, + &vault_id, + &generation, + &self.inner.profile_hash, + ) + .map_err(|e| EngineError::Other(format!("genesis verify: {e}")))?; + let mut keyring = Keyring::new(); + keyring + .insert_fresh(1) + .map_err(|e| EngineError::Other(e.to_string()))?; + + // Publish order (RFC §5): journal keys and genesis before sending + // the membership record, then deliver the recipient envelopes. + // Recovery material stays protected locally until the user confirms + // saving the kit. Interrupted publication resumes the same intent; + // content remains paused until delivery and confirmation complete. + { + let mut guarded = lock(&self.inner.guarded); + guarded.state.vault = Some(PinnedVault { + vault_id: Hex::of(&vault_id), + generation: Hex::of(&generation), + profile_hash: Hex::of(&self.inner.profile_hash), + membership: vec![encode_base64(&genesis)], + }); + guarded.state.keyring = Some(Secret::of(&keyring.encode())); + guarded.state.enrollment = None; + guarded.state.pending_membership = Some(encode_base64(&genesis)); + guarded.state.setup_recovery = Some(Secret::of(recovery.expose_secret())); + guarded.state.owed_envelopes = vec![ + (Hex::of(&identity.device_id), 1), + (Hex::of(&state.recovery_authority_id()), 1), + ]; + self.commit(&mut guarded)?; + guarded.remote_vault = Some(true); + } + self.finish_pending_membership(&client).await?; + self.setup_kit() + } + + // ── refresh ────────────────────────────────────────────────────────────── + + /// Reconcile with the control plane: pull new membership records, learn + /// of revocation, fetch a keyring update, publish owed envelopes, and + /// complete a pending enrollment. + pub async fn refresh(&self) -> Result { + let _ops = self.inner.ops.lock().await; + let client = self.client()?.clone(); + if lock(&self.inner.guarded).locked.is_some() { + return Ok(self.status()); + } + self.finish_pending_membership(&client).await?; + let descriptor = client.descriptor().await?; + { + let mut guarded = lock(&self.inner.guarded); + let discovered = descriptor.is_some() && guarded.remote_vault != Some(true); + guarded.remote_vault = Some(descriptor.is_some() || guarded.remote_vault == Some(true)); + if discovered { + guarded.encryption_required = true; + guarded.state.version = 1; + self.commit(&mut guarded)?; + } + } + let enrolled = lock(&self.inner.guarded).trust.is_some(); + if enrolled { + self.pull_membership(&client).await?; + self.pull_keyring(&client).await?; + self.publish_owed(&client).await?; + } else if lock(&self.inner.guarded).state.enrollment.is_some() { + self.poll_enrollment(&client).await?; + } + self.publish_status(); + Ok(self.status()) + } + + async fn pull_membership(&self, client: &VaultClient) -> Result<(), EngineError> { + let head_seq = lock(&self.inner.guarded) + .trust + .as_ref() + .map(|t| t.head().sequence() as i64) + .unwrap_or(-1); + let page = client.membership_after(head_seq).await?; + if page.records.is_empty() { + return Ok(()); + } + let mut guarded = lock(&self.inner.guarded); + let Some(trust) = guarded.trust.as_mut() else { + return Ok(()); + }; + let mut appended = Vec::new(); + for encoded in &page.records { + let bytes = decode_base64(encoded) + .ok_or_else(|| EngineError::Other("malformed membership record".into()))?; + match trust.head().apply(&bytes) { + Ok(next) => { + trust.history.push(Revision { + hash: *next.hash(), + state: next, + }); + appended.push(encoded.clone()); + } + Err(err) => { + // A record the server serves that our pinned history + // rejects: stop advancing and surface it. Nothing local + // is discarded. + guarded.verification_failure = + Some(format!("membership record rejected: {err}")); + break; + } + } + } + if !appended.is_empty() { + if let Some(vault) = guarded.state.vault.as_mut() { + vault.membership.extend(appended); + if vault.membership.len() > MAX_MEMBERSHIP_RECORDS { + guarded.verification_failure = Some("membership history too long".into()); + } + } + self.commit(&mut guarded)?; + } + Ok(()) + } + + /// Fetch this device's keyring envelope when the head epoch is ahead of + /// what the keyring holds. + async fn pull_keyring(&self, client: &VaultClient) -> Result<(), EngineError> { + let (device_id, needs) = { + let guarded = lock(&self.inner.guarded); + let Some(trust) = guarded.trust.as_ref() else { + return Ok(()); + }; + let head = trust.head(); + ( + trust.device_id, + head.active_device(&trust.device_id).is_some() + && trust.keyring.epoch_key(head.epoch()).is_none(), + ) + }; + if !needs { + return Ok(()); + } + let Some(envelope) = client.get_envelope(&device_id).await? else { + return Ok(()); + }; + let mut guarded = lock(&self.inner.guarded); + let Some(trust) = guarded.trust.as_mut() else { + return Ok(()); + }; + match open_keyring_envelope(trust, &envelope, RecipientKind::Device, &device_id) { + Ok(keyring) => { + trust + .keyring + .merge(&keyring) + .map_err(|e| EngineError::Other(format!("keyring merge: {e}")))?; + guarded.state.keyring = Some(Secret::of(&trust.keyring.encode())); + self.commit(&mut guarded)?; + } + Err(reason) => { + tracing::warn!(reason, "vault: keyring envelope rejected"); + } + } + Ok(()) + } + + /// Envelopes this device promised after a rotation it authored. + async fn publish_owed(&self, client: &VaultClient) -> Result<(), EngineError> { + let owed = lock(&self.inner.guarded).state.owed_envelopes.clone(); + let mut done = Vec::new(); + for (recipient, epoch) in owed { + let recipient_id = recipient + .decode::<16>() + .ok_or_else(|| EngineError::Other("invalid owed recipient".into()))?; + let slot = format!("{}:{epoch}", recipient.0); + let sealed = { + let mut guarded = lock(&self.inner.guarded); + if guarded.locked.is_some() + || guarded.verification_failure.is_some() + || guarded.state.pending_membership.is_some() + { + return Err(EngineError::Other( + "vault update is not durable or verified".into(), + )); + } + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("vault keys are unavailable".into()))?; + let cached = guarded + .state + .owed_envelope_records + .get(&slot) + .and_then(|encoded| decode_base64(encoded)) + .filter(|bytes| { + UnverifiedRecord::parse(bytes, envelope::MAX_ENVELOPE_PAYLOAD_BYTES) + .is_ok_and(|record| { + record.untrusted_binding().membership_hash == *trust.head().hash() + }) + }); + let sealed = match cached { + Some(record) => Some(Ok(record)), + None => seal_keyring_for(trust, &recipient_id, epoch), + }; + if let Some(Ok(record)) = &sealed { + let encoded = encode_base64(record); + if guarded.state.owed_envelope_records.get(&slot) != Some(&encoded) { + guarded.state.owed_envelope_records.insert(slot, encoded); + self.commit(&mut guarded)?; + } + } + sealed + }; + match sealed { + Some(Ok(record)) => match client.put_envelope(&recipient_id, record).await { + Ok(()) => done.push((recipient, epoch)), + Err(err) => tracing::warn!(error = %err, "vault: owed envelope publish failed"), + }, + Some(Err(reason)) => return Err(EngineError::Other(reason)), + None => done.push((recipient, epoch)), + } + } + if !done.is_empty() { + let mut guarded = lock(&self.inner.guarded); + guarded + .state + .owed_envelopes + .retain(|entry| !done.contains(entry)); + for (recipient, epoch) in done { + guarded + .state + .owed_envelope_records + .remove(&format!("{}:{epoch}", recipient.0)); + } + self.commit(&mut guarded)?; + } + Ok(()) + } + + // ── enrollment (new device) ────────────────────────────────────────────── + + /// Ask an existing device to approve this one. Returns the request id + /// and the comparison code the user must match on the approving device. + pub async fn request_enrollment(&self) -> Result<(String, String), EngineError> { + let _ops = self.inner.ops.lock().await; + let client = self.client()?.clone(); + if lock(&self.inner.guarded).trust.is_some() { + return Err(EngineError::Other("this device is already enrolled".into())); + } + let Some(descriptor) = client.descriptor().await? else { + return Err(EngineError::Other( + "no vault exists for this account yet; set up encryption first".into(), + )); + }; + let vault_id = descriptor + .vault_id + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad vault id".into()))?; + let generation = descriptor + .generation + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad generation".into()))?; + // The chain is fetched now (unverified against any pin) so the + // pairing code can bind its genesis hash. It is pinned only once the + // approver's decision — made against ITS pinned genesis — lands as a + // membership record that admits exactly our keys. + let records = fetch_all_membership(&client).await?; + let genesis_hash = policy::membership_hash( + &records + .first() + .and_then(|r| decode_base64(r)) + .ok_or_else(|| EngineError::Other("empty membership".into()))?, + ); + verify_chain(&records, &vault_id, &generation, &self.inner.profile_hash) + .map_err(|reason| EngineError::Other(format!("remote vault rejected: {reason}")))?; + let identity = { + let mut guarded = lock(&self.inner.guarded); + self.ensure_identity(&mut guarded)? + }; + let mut request_id = [0u8; 16]; + zeron_crypto::fill_random(&mut request_id) + .map_err(|e| EngineError::Other(e.to_string()))?; + let request = EnrollmentRequest { + vault_id, + request_id, + device_id: identity.device_id, + signing_key: identity.signing_key(), + encryption_key: *identity.encryption.public_key().as_bytes(), + }; + let proof = request + .sign(&identity.signer) + .map_err(|e| EngineError::Other(e.to_string()))?; + let info = client.create_enrollment(&request, &proof).await?; + let code = request.pairing_code(&genesis_hash); + { + let mut guarded = lock(&self.inner.guarded); + guarded.state.vault = Some(PinnedVault { + vault_id: Hex::of(&vault_id), + generation: Hex::of(&generation), + profile_hash: Hex::of(&self.inner.profile_hash), + membership: records, + }); + guarded.state.enrollment = Some(PendingEnrollment { + request_id: Hex::of(&request_id), + created_at: info.created_at, + }); + guarded.remote_vault = Some(true); + self.commit(&mut guarded)?; + } + self.publish_status(); + Ok((Hex::of(&request_id).0, code)) + } + + pub async fn cancel_enrollment(&self) -> Result<(), EngineError> { + let _ops = self.inner.ops.lock().await; + let pending = lock(&self.inner.guarded).state.enrollment.clone(); + if let (Some(pending), Ok(client)) = (pending, self.client()) + && let Some(id) = pending.request_id.decode::<16>() + { + let _ = client.reject_enrollment(&id).await; + } + let mut guarded = lock(&self.inner.guarded); + guarded.state.enrollment = None; + if guarded.trust.is_none() { + guarded.state.vault = None; + } + self.commit(&mut guarded)?; + drop(guarded); + self.publish_status(); + Ok(()) + } + + async fn poll_enrollment(&self, client: &VaultClient) -> Result<(), EngineError> { + let (pending, identity) = { + let guarded = lock(&self.inner.guarded); + let Some(pending) = guarded.state.enrollment.clone() else { + return Ok(()); + }; + (pending, identity_of(&guarded.state)?) + }; + let Some(request_id) = pending.request_id.decode::<16>() else { + return Ok(()); + }; + let info = client.enrollment(&request_id).await?; + match info.as_ref().map(|i| i.status.as_str()) { + Some("approved") => {} + Some("pending") => return Ok(()), + _ => { + // Rejected, expired, or gone: clear the request; the user may + // ask again. Our identity stays (nothing about it leaked). + let mut guarded = lock(&self.inner.guarded); + guarded.state.enrollment = None; + guarded.state.vault = None; + self.commit(&mut guarded)?; + return Ok(()); + } + } + let (vault_id, generation) = { + let guarded = lock(&self.inner.guarded); + let vault = guarded + .state + .vault + .as_ref() + .ok_or_else(|| EngineError::Other("missing pinned vault".into()))?; + ( + vault + .vault_id + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad vault".into()))?, + vault + .generation + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad vault".into()))?, + ) + }; + let records = fetch_all_membership(client).await?; + let head = verify_chain(&records, &vault_id, &generation, &self.inner.profile_hash) + .map_err(|reason| EngineError::Other(format!("remote vault rejected: {reason}")))?; + let entry = head + .active_device(&identity.device_id) + .ok_or_else(|| EngineError::Other("approval did not admit this device".into()))?; + if entry.signing_key != identity.signing_key() + || entry.encryption_key != *identity.encryption.public_key().as_bytes() + { + return Err(EngineError::Other( + "membership names this device with different keys; refusing".into(), + )); + } + let Some(envelope) = client.get_envelope(&identity.device_id).await? else { + return Ok(()); // approver still publishing; try again later + }; + let mut guarded = lock(&self.inner.guarded); + guarded.state.vault = Some(PinnedVault { + vault_id: Hex::of(&vault_id), + generation: Hex::of(&generation), + profile_hash: Hex::of(&self.inner.profile_hash), + membership: records, + }); + // An empty keyring makes the pinned history rebuildable; the real + // keyring replaces it once the envelope opens against that history. + guarded.state.keyring = Some(Secret::of(&Keyring::new().encode())); + let mut trust = rebuild_trust(&guarded.state, &self.inner.profile_hash) + .map_err(EngineError::Other)? + .ok_or_else(|| EngineError::Other("trust rebuild failed".into()))?; + let keyring = match open_keyring_envelope( + &trust, + &envelope, + RecipientKind::Device, + &identity.device_id, + ) { + Ok(keyring) => keyring, + Err(reason) => { + guarded.state.keyring = None; + return Err(EngineError::Other(format!( + "keyring envelope rejected: {reason}" + ))); + } + }; + trust.keyring = keyring; + guarded.state.keyring = Some(Secret::of(&trust.keyring.encode())); + guarded.state.enrollment = None; + self.commit(&mut guarded)?; + guarded.trust = Some(trust); + Ok(()) + } + + // ── approval (existing device) ─────────────────────────────────────────── + + /// Pending requests with the comparison code computed from OUR pinned + /// genesis and the keys the server reports (the user compares it against + /// the code shown on the pending device). + pub async fn pending_requests(&self) -> Result, EngineError> { + let client = self.client()?.clone(); + let (vault_id, genesis) = { + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("this device is not enrolled".into()))?; + (*trust.head().vault_id(), *trust.head().genesis_hash()) + }; + let mut out = Vec::new(); + for info in client.list_enrollments().await? { + let Some(request) = request_from_info(&info, vault_id) else { + continue; + }; + out.push(PendingRequest { + request_id: info.request_id.0.clone(), + device_id: info.device_id.0.clone(), + pairing_code: request.pairing_code(&genesis), + expires_at: info.expires_at, + }); + } + Ok(out) + } + + /// Admit a pending device. `confirmed_code` must equal the code the + /// user read from the pending device; it is re-derived here from the + /// server-reported keys so a substituted key cannot pass. + pub async fn approve( + &self, + request_id_hex: &str, + confirmed_code: &str, + ) -> Result<(), EngineError> { + let _ops = self.inner.ops.lock().await; + let client = self.client()?.clone(); + let request_id = Hex(request_id_hex.to_string()) + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad request id".into()))?; + let info = client + .enrollment(&request_id) + .await? + .ok_or_else(|| EngineError::Other("enrollment request not found".into()))?; + if info.status != "pending" { + return Err(EngineError::Other(format!("request is {}", info.status))); + } + let (vault_id, genesis, identity) = { + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("this device is not enrolled".into()))?; + ( + *trust.head().vault_id(), + *trust.head().genesis_hash(), + identity_of(&guarded.state)?, + ) + }; + let request = request_from_info(&info, vault_id) + .ok_or_else(|| EngineError::Other("malformed enrollment request".into()))?; + let expected = request.pairing_code(&genesis); + if normalize_code(confirmed_code) != normalize_code(&expected) { + return Err(EngineError::Other( + "the code does not match; do not approve a device whose code differs".into(), + )); + } + // Make sure our view of the head is current before authoring on it. + self.finish_pending_membership(&client).await?; + self.pull_membership(&client).await?; + if !self.is_ready() { + return Err(EngineError::Other("vault is not ready for approval".into())); + } + let (record, next_state) = { + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("not enrolled".into()))?; + let head = trust.head(); + if head.active_device(&trust.device_id).is_none() { + return Err(EngineError::Other("this device is revoked".into())); + } + let mut payload = head.next_payload(Operation::AddDevice); + payload.devices.push(request.device_entry()); + let binding = policy::policy_binding( + *head.vault_id(), + *head.generation(), + payload.epoch, + trust.device_id, + *head.hash(), + ); + let record = policy::encode_policy(&binding, &payload, &trust.signer) + .map_err(|e| EngineError::Other(format!("add device: {e}")))?; + let next = head + .apply(&record) + .map_err(|e| EngineError::Other(format!("add device verify: {e}")))?; + (record, next) + }; + let _ = identity; + { + let mut guarded = lock(&self.inner.guarded); + if let Some(vault) = guarded.state.vault.as_mut() { + vault.membership.push(encode_base64(&record)); + } + if let Some(trust) = guarded.trust.as_mut() { + trust.history.push(Revision { + hash: *next_state.hash(), + state: next_state.clone(), + }); + } + // The envelope is owed until it lands; a crash here retries it. + guarded + .state + .owed_envelopes + .push((Hex::of(&request.device_id), next_state.epoch())); + guarded.state.pending_membership = Some(encode_base64(&record)); + guarded.state.pending_approval = + Some((Hex::of(&request_id), next_state.sequence() as i64)); + self.commit(&mut guarded)?; + } + self.finish_pending_membership(&client).await + } + + pub async fn reject(&self, request_id_hex: &str) -> Result<(), EngineError> { + let client = self.client()?.clone(); + let request_id = Hex(request_id_hex.to_string()) + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad request id".into()))?; + client.reject_enrollment(&request_id).await + } + + // ── revocation ─────────────────────────────────────────────────────────── + + /// Revoke `device_id_hex` and rotate to a fresh write epoch: post the + /// signed transition, then publish the new keyring to every retained + /// device and the recovery authority (owed until each lands). + pub async fn revoke(&self, device_id_hex: &str) -> Result<(), EngineError> { + let _ops = self.inner.ops.lock().await; + let client = self.client()?.clone(); + let target = Hex(device_id_hex.to_string()) + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad device id".into()))?; + self.finish_pending_membership(&client).await?; + self.pull_membership(&client).await?; + if !self.is_ready() { + return Err(EngineError::Other( + "vault is not ready for revocation".into(), + )); + } + let (record, next_state, next_epoch, fresh_key) = { + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("not enrolled".into()))?; + let head = trust.head(); + if head.active_device(&trust.device_id).is_none() { + return Err(EngineError::Other("this device is revoked".into())); + } + if target == trust.device_id { + return Err(EngineError::Other( + "use another approved device to revoke this device".into(), + )); + } + if head.active_device(&target).is_none() { + return Err(EngineError::Other( + "that device is not an active member".into(), + )); + } + let mut payload = head.next_payload(Operation::RevokeDevice); + for device in payload.devices.iter_mut() { + if device.device_id == target { + device.status = DeviceStatus::Revoked; + } + } + let binding = policy::policy_binding( + *head.vault_id(), + *head.generation(), + payload.epoch, + trust.device_id, + *head.hash(), + ); + let record = policy::encode_policy(&binding, &payload, &trust.signer) + .map_err(|e| EngineError::Other(format!("revoke: {e}")))?; + let next = head + .apply(&record) + .map_err(|e| EngineError::Other(format!("revoke verify: {e}")))?; + let mut fresh = [0u8; 32]; + zeron_crypto::fill_random(&mut fresh).map_err(|e| EngineError::Other(e.to_string()))?; + (record, next.clone(), next.epoch(), fresh) + }; + // Journal locally: new epoch key + owed envelopes for every retained + // recipient. The revoked device is deliberately not a recipient. + { + let mut guarded = lock(&self.inner.guarded); + if let Some(vault) = guarded.state.vault.as_mut() { + vault.membership.push(encode_base64(&record)); + } + let recipients: Vec<[u8; 16]> = next_state + .devices() + .iter() + .filter(|d| d.status == DeviceStatus::Active) + .map(|d| d.device_id) + .chain(std::iter::once(next_state.recovery_authority_id())) + .collect(); + if let Some(trust) = guarded.trust.as_mut() { + trust + .keyring + .insert(next_epoch, &fresh_key) + .map_err(|e| EngineError::Other(format!("keyring: {e}")))?; + trust.history.push(Revision { + hash: *next_state.hash(), + state: next_state.clone(), + }); + guarded.state.keyring = Some(Secret::of(&trust.keyring.encode())); + } + for recipient in recipients { + guarded + .state + .owed_envelopes + .push((Hex::of(&recipient), next_epoch)); + } + guarded.state.pending_membership = Some(encode_base64(&record)); + self.commit(&mut guarded)?; + } + self.finish_pending_membership(&client).await + } + + // ── recovery ───────────────────────────────────────────────────────────── + + /// Rejoin with the recovery kit and no existing device: verify the + /// server's chain against the kit's authority, open the recovery + /// envelope, and sign a recovery transition that admits this device + /// under a fresh epoch. `expected_genesis` (from the recovery file) pins + /// the vault when available. + pub async fn recover( + &self, + kit_text: &str, + expected_genesis: Option<[u8; 32]>, + ) -> Result<(), EngineError> { + let _ops = self.inner.ops.lock().await; + let client = self.client()?.clone(); + if lock(&self.inner.guarded).trust.is_some() { + return Err(EngineError::Other("this device is already enrolled".into())); + } + let secret = RecoverySecret::from_kit(kit_text) + .map_err(|e| EngineError::Other(format!("recovery key: {e}")))?; + let recovery_signer = secret + .signer() + .map_err(|e| EngineError::Other(e.to_string()))?; + let recovery_encryption = secret + .encryption_key() + .map_err(|e| EngineError::Other(e.to_string()))?; + let Some(descriptor) = client.descriptor().await? else { + return Err(EngineError::Other( + "no vault exists for this account".into(), + )); + }; + let vault_id = descriptor + .vault_id + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad vault id".into()))?; + let generation = descriptor + .generation + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad generation".into()))?; + let records = fetch_all_membership(&client).await?; + let head = verify_chain(&records, &vault_id, &generation, &self.inner.profile_hash) + .map_err(|reason| EngineError::Other(format!("remote vault rejected: {reason}")))?; + if let Some(expected) = expected_genesis + && *head.genesis_hash() != expected + { + return Err(EngineError::Other( + "the vault on the server is not the one named in the recovery file".into(), + )); + } + let authority = head.recovery_authority_id(); + if authority != *recovery_signer.author_id() { + return Err(EngineError::Other( + "this recovery key does not belong to the current vault (it may have been replaced)" + .into(), + )); + } + let envelope = client + .get_envelope(&authority) + .await? + .ok_or_else(|| EngineError::Other("no recovery envelope is published".into()))?; + let history = replay_chain(&records, &vault_id, &generation, &self.inner.profile_hash) + .map_err(EngineError::Other)?; + let keyring = open_envelope_against( + &history, + &envelope, + RecipientKind::Recovery, + &authority, + &recovery_encryption, + ) + .map_err(|reason| EngineError::Other(format!("recovery envelope rejected: {reason}")))?; + + let identity = { + let mut guarded = lock(&self.inner.guarded); + self.ensure_identity(&mut guarded)? + }; + let mut payload = head.next_payload(Operation::RecoveryTransition); + payload.devices.push(identity.entry()); + let binding = + policy::policy_binding(vault_id, generation, payload.epoch, authority, *head.hash()); + let record = policy::encode_policy(&binding, &payload, &recovery_signer) + .map_err(|e| EngineError::Other(format!("recovery transition: {e}")))?; + let next = head + .apply(&record) + .map_err(|e| EngineError::Other(format!("recovery verify: {e}")))?; + let mut keyring = keyring; + keyring + .insert_fresh(next.epoch()) + .map_err(|e| EngineError::Other(e.to_string()))?; + let recipients: Vec<[u8; 16]> = next + .devices() + .iter() + .filter(|d| d.status == DeviceStatus::Active) + .map(|d| d.device_id) + .chain(std::iter::once(next.recovery_authority_id())) + .collect(); + { + let mut records = records; + records.push(encode_base64(&record)); + let mut guarded = lock(&self.inner.guarded); + guarded.state.vault = Some(PinnedVault { + vault_id: Hex::of(&vault_id), + generation: Hex::of(&generation), + profile_hash: Hex::of(&self.inner.profile_hash), + membership: records, + }); + guarded.state.keyring = Some(Secret::of(&keyring.encode())); + guarded.state.enrollment = None; + for recipient in recipients { + guarded + .state + .owed_envelopes + .push((Hex::of(&recipient), next.epoch())); + } + guarded.state.pending_membership = Some(encode_base64(&record)); + self.commit(&mut guarded)?; + guarded.trust = rebuild_trust(&guarded.state, &self.inner.profile_hash) + .map_err(EngineError::Other)?; + guarded.remote_vault = Some(true); + } + self.finish_pending_membership(&client).await + } + + // ── content key material ───────────────────────────────────────────────── + + /// Sealing material for `object_id` under the current epoch. Creates and + /// publishes the object's key for this epoch when none exists; the + /// envelope is durable on the control plane before this returns. + pub async fn seal_material(&self, object_id: [u8; 16]) -> Result { + let client = self.client()?.clone(); + let (epoch, cached) = { + let guarded = lock(&self.inner.guarded); + if guarded.content_blocked() { + return Err(EngineError::Other( + "vault verification or storage is unavailable".into(), + )); + } + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("vault not ready".into()))?; + let head = trust.head(); + if head.active_device(&trust.device_id).is_none() { + return Err(EngineError::Other("this device is revoked".into())); + } + if trust.keyring.epoch_key(head.epoch()).is_none() { + return Err(EngineError::Other("waiting for encryption keys".into())); + } + let epoch = head.epoch(); + let cached = trust.object_keys.get(&(object_id, epoch)).cloned(); + (epoch, cached) + }; + if cached.is_none() { + self.fetch_object_keys(&client, object_id).await?; + } + let cached = cached.or_else(|| { + lock(&self.inner.guarded) + .trust + .as_ref() + .and_then(|t| t.object_keys.get(&(object_id, epoch)).cloned()) + }); + let key = match cached { + Some(key) => key, + None => self.create_object_key(&client, object_id, epoch).await?, + }; + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("vault not ready".into()))?; + let head = trust.head(); + if head.epoch() != epoch + || head.active_device(&trust.device_id).is_none() + || guarded.content_blocked() + { + return Err(EngineError::Other("vault state changed; retry".into())); + } + Ok(ChatKeyMaterial { + binding: head.content_binding(object_id, trust.device_id), + key, + signer: trust.signer.clone(), + }) + } + + async fn fetch_object_keys( + &self, + client: &VaultClient, + object_id: [u8; 16], + ) -> Result<(), EngineError> { + let entries = client.object_keys(&object_id).await?; + let mut guarded = lock(&self.inner.guarded); + let mut changed = false; + { + let Guarded { state, trust, .. } = &mut *guarded; + let Some(trust) = trust.as_mut() else { + return Ok(()); + }; + for entry in entries { + let Some(bytes) = decode_base64(&entry.record) else { + continue; + }; + match unwrap_object_key_against(trust, &bytes, object_id) { + Ok(key) => { + let epoch = key.scope().epoch; + if let std::collections::btree_map::Entry::Vacant(slot) = + trust.object_keys.entry((object_id, epoch)) + { + cache_object_key(state, object_id, &key); + slot.insert(Arc::new(key)); + changed = true; + } + } + Err(reason) => tracing::debug!(reason, "vault: object key envelope skipped"), + } + } + } + if changed { + self.commit(&mut guarded)?; + } + Ok(()) + } + + async fn create_object_key( + &self, + client: &VaultClient, + object_id: [u8; 16], + epoch: u64, + ) -> Result, EngineError> { + let (record, key) = { + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("vault not ready".into()))?; + let head = trust.head(); + let binding = head.envelope_binding(object_id, epoch, trust.device_id); + let key = ContentKey::generate(KeyScope::from(&binding)) + .map_err(|e| EngineError::Other(e.to_string()))?; + let epoch_key = trust + .keyring + .epoch_key(epoch) + .ok_or_else(|| EngineError::Other("waiting for encryption keys".into()))?; + let record = envelope::wrap_object_key(&binding, epoch_key, &key, &trust.signer) + .map_err(|e| EngineError::Other(format!("object key: {e}")))?; + (record.into_encoded(), key) + }; + let published = client.put_object_key(&object_id, record).await?; + let key = if published.adopted_existing { + let guarded = lock(&self.inner.guarded); + let trust = guarded + .trust + .as_ref() + .ok_or_else(|| EngineError::Other("vault not ready".into()))?; + unwrap_object_key_against(trust, &published.record, object_id).map_err(|reason| { + EngineError::Other(format!("adopted object key rejected: {reason}")) + })? + } else { + key + }; + let key = Arc::new(key); + let mut guarded = lock(&self.inner.guarded); + if let Some(trust) = guarded.trust.as_mut() { + trust.object_keys.insert((object_id, epoch), key.clone()); + } + cache_object_key(&mut guarded.state, object_id, &key); + self.commit(&mut guarded)?; + Ok(key) + } + + /// This device's Ed25519 public key (verifies its own outbox records). + pub fn signing_public_key(&self) -> Option<[u8; 32]> { + lock(&self.inner.guarded) + .trust + .as_ref() + .map(|t| t.signing_key()) + } + + /// The binding a record sealed RIGHT NOW for `object_id` would carry. + pub fn current_content_binding(&self, object_id: [u8; 16]) -> Option { + let guarded = lock(&self.inner.guarded); + if guarded.content_blocked() { + return None; + } + let trust = guarded.trust.as_ref()?; + let head = trust.head(); + head.active_device(&trust.device_id)?; + trust.keyring.epoch_key(head.epoch())?; + Some(head.content_binding(object_id, trust.device_id)) + } + + /// Kick a background key refresh (membership + keyring + this object's + /// keys). Callers that hit `KeyUnavailable` on a synchronous path use + /// this; the status watch tells them when to resume. + pub fn spawn_key_refresh(&self, object_id: [u8; 16]) { + let service = self.clone(); + tokio::spawn(async move { + if let Err(err) = service.refresh().await { + tracing::debug!(error = %err, "vault: background refresh failed"); + } + if let Ok(client) = service.client() { + let client = client.clone(); + if let Err(err) = service.fetch_object_keys(&client, object_id).await { + tracing::debug!(error = %err, "vault: object key prefetch failed"); + } + } + service.publish_status(); + }); + } + + /// Synchronous twin of [`Self::open_material`]: answers from pinned + /// history and cached keys only; `KeyUnavailable` means a fetch is needed. + pub fn open_material_cached( + &self, + object_id: [u8; 16], + untrusted: &RecordBinding, + ) -> Result { + let (binding, author_public_key, have_epoch, cached) = + self.trusted_context(object_id, untrusted)?; + if !have_epoch { + return Err(OpenFailure::KeyUnavailable); + } + let key = cached.ok_or(OpenFailure::KeyUnavailable)?; + Ok(OpenContext { + binding, + key, + author_public_key, + }) + } + + fn trusted_context( + &self, + object_id: [u8; 16], + untrusted: &RecordBinding, + ) -> Result { + if untrusted.kind != RecordKind::Content || untrusted.object_id != object_id { + return Err(OpenFailure::NotAuthorized); + } + let guarded = lock(&self.inner.guarded); + if guarded.content_blocked() { + return Err(OpenFailure::Unavailable); + } + let trust = guarded.trust.as_ref().ok_or(OpenFailure::Unavailable)?; + let revision = trust + .revision(&untrusted.membership_hash) + .ok_or(OpenFailure::NotAuthorized)?; + let state = &revision.state; + if state.epoch() != untrusted.epoch + || *state.vault_id() != untrusted.vault_id + || *state.generation() != untrusted.generation + { + return Err(OpenFailure::NotAuthorized); + } + let author = state + .active_device(&untrusted.author_id) + .ok_or(OpenFailure::NotAuthorized)?; + Ok(( + state.content_binding(object_id, author.device_id), + author.signing_key, + trust.keyring.epoch_key(untrusted.epoch).is_some(), + trust + .object_keys + .get(&(object_id, untrusted.epoch)) + .cloned(), + )) + } + + /// Opening material for a record whose UNTRUSTED binding was parsed + /// from the wire. The returned binding is rebuilt from pinned history; + /// callers verify the record against it, never against the parsed one. + pub async fn open_material( + &self, + object_id: [u8; 16], + untrusted: &RecordBinding, + ) -> Result { + let trusted = self.trusted_context(object_id, untrusted)?; + let (binding, author_public_key, have_epoch, cached) = trusted; + if !have_epoch { + return Err(OpenFailure::KeyUnavailable); + } + let key = match cached { + Some(key) => key, + None => { + let client = self.client().map_err(|_| OpenFailure::Unavailable)?.clone(); + self.fetch_object_keys(&client, object_id) + .await + .map_err(|_| OpenFailure::KeyUnavailable)?; + lock(&self.inner.guarded) + .trust + .as_ref() + .and_then(|t| t.object_keys.get(&(object_id, untrusted.epoch)).cloned()) + .ok_or(OpenFailure::KeyUnavailable)? + } + }; + Ok(OpenContext { + binding, + key, + author_public_key, + }) + } +} + +impl zeron_rpc::ChannelAuthority for VaultService { + fn required(&self) -> bool { + self.is_enrolled() + } + + fn local(&self) -> Result { + self.channel_local() + } + + fn accept(&self, peer: &zeron_crypto::channel::PeerIdentity) -> bool { + self.channel_accepts(peer) + } +} + +// ── helpers ───────────────────────────────────────────────────────────────── + +struct Identity { + device_id: [u8; 16], + signer: Arc, + encryption: HpkePrivateKey, +} + +impl Identity { + fn signing_key(&self) -> [u8; 32] { + self.signer.public_key().try_into().expect("32-byte key") + } + + fn entry(&self) -> policy::DeviceEntry { + policy::DeviceEntry { + device_id: self.device_id, + signing_key: self.signing_key(), + encryption_key: *self.encryption.public_key().as_bytes(), + status: DeviceStatus::Active, + } + } +} + +fn identity_of(state: &LocalVaultState) -> Result { + let device = state + .device + .as_ref() + .ok_or_else(|| EngineError::Other("no device identity".into()))?; + let device_id = device + .device_id + .decode::<16>() + .ok_or_else(|| EngineError::Other("bad device id".into()))?; + let seed = device + .signing_seed + .decode::<32>() + .ok_or_else(|| EngineError::Other("bad signing seed".into()))?; + let encryption = device + .encryption_secret + .decode::<32>() + .ok_or_else(|| EngineError::Other("bad encryption secret".into()))?; + Ok(Identity { + device_id, + signer: Arc::new( + DeviceSigner::from_seed(device_id, &seed) + .map_err(|e| EngineError::Other(e.to_string()))?, + ), + encryption: HpkePrivateKey::from_bytes(&encryption) + .map_err(|e| EngineError::Other(e.to_string()))?, + }) +} + +fn replay_chain( + records: &[String], + vault_id: &[u8; 16], + generation: &[u8; 16], + profile_hash: &[u8; 32], +) -> Result, String> { + if records.len() > MAX_MEMBERSHIP_RECORDS { + return Err("membership history too long".into()); + } + let mut history = Vec::with_capacity(records.len()); + for (index, encoded) in records.iter().enumerate() { + let bytes = decode_base64(encoded).ok_or("malformed membership record")?; + let state = if index == 0 { + MembershipState::from_genesis(&bytes, vault_id, generation, profile_hash) + .map_err(|e| format!("genesis: {e}"))? + } else { + let previous: &Revision = history.last().expect("genesis present"); + previous + .state + .apply(&bytes) + .map_err(|e| format!("record {index}: {e}"))? + }; + history.push(Revision { + hash: *state.hash(), + state, + }); + } + if history.is_empty() { + return Err("empty membership".into()); + } + Ok(history) +} + +fn verify_chain( + records: &[String], + vault_id: &[u8; 16], + generation: &[u8; 16], + profile_hash: &[u8; 32], +) -> Result { + replay_chain(records, vault_id, generation, profile_hash) + .map(|h| h.last().expect("non-empty").state.clone()) +} + +fn rebuild_trust( + state: &LocalVaultState, + profile_hash: &[u8; 32], +) -> Result, String> { + let Some(vault) = state.vault.as_ref() else { + return Ok(None); + }; + // A pinned vault without a keyring is a pending enrollment, not trust. + let Some(keyring_bytes) = state.keyring.as_ref() else { + return Ok(None); + }; + let identity = identity_of(state).map_err(|e| e.to_string())?; + let vault_id = vault.vault_id.decode::<16>().ok_or("bad vault id")?; + let generation = vault.generation.decode::<16>().ok_or("bad generation")?; + if vault.profile_hash.decode::<32>() != Some(*profile_hash) { + return Err("vault state belongs to another profile".into()); + } + let history = replay_chain(&vault.membership, &vault_id, &generation, profile_hash)?; + let keyring = Keyring::decode(&keyring_bytes.bytes().ok_or("bad keyring")?) + .map_err(|e| format!("keyring: {e}"))?; + let mut object_keys = BTreeMap::new(); + for (object_hex, entries) in &state.object_keys { + let Some(object_id) = Hex(object_hex.clone()).decode::<16>() else { + continue; + }; + for entry in entries { + let scope = KeyScope { + vault_id, + generation, + epoch: entry.epoch, + object_id, + }; + if let (Some(id), Some(bytes)) = (entry.key_id.decode::<16>(), entry.key.bytes()) + && let Ok(key) = ContentKey::from_bytes(scope, id, &bytes) + { + object_keys.insert((object_id, entry.epoch), Arc::new(key)); + } + } + } + Ok(Some(Trust { + device_id: identity.device_id, + signer: identity.signer, + encryption: identity.encryption, + history, + keyring, + object_keys, + })) +} + +fn cache_object_key(state: &mut LocalVaultState, object_id: [u8; 16], key: &ContentKey) { + let entries = state.object_keys.entry(Hex::of(&object_id).0).or_default(); + let epoch = key.scope().epoch; + if entries.iter().any(|e| e.epoch == epoch) { + return; + } + entries.push(CachedObjectKey { + epoch, + key_id: Hex::of(key.identifier()), + key: Secret::of(key.expose_secret()), + }); +} + +/// Verify an envelope record against pinned history: its membership hash +/// must name a revision at which its author was active. +fn envelope_context( + history: &[Revision], + encoded: &[u8], + object_id: &[u8; 16], +) -> Result<(RecordBinding, [u8; 32]), String> { + let parsed = UnverifiedRecord::parse(encoded, envelope::MAX_ENVELOPE_PAYLOAD_BYTES) + .map_err(|e| format!("parse: {e}"))?; + let untrusted = *parsed.untrusted_binding(); + let revision = history + .iter() + .rev() + .find(|r| r.hash == untrusted.membership_hash) + .ok_or("unknown membership hash")?; + let state = &revision.state; + let author = state + .active_device(&untrusted.author_id) + .ok_or("author not active at that revision")?; + if untrusted.epoch > state.epoch() || untrusted.object_id != *object_id { + return Err("envelope context mismatch".into()); + } + Ok(( + state.envelope_binding(*object_id, untrusted.epoch, author.device_id), + author.signing_key, + )) +} + +fn open_envelope_against( + history: &[Revision], + encoded: &[u8], + kind: RecipientKind, + recipient: &[u8; 16], + recipient_key: &HpkePrivateKey, +) -> Result { + let (binding, author_key) = envelope_context(history, encoded, &POLICY_OBJECT_ID)?; + envelope::open_keyring( + encoded, + &binding, + kind, + recipient, + recipient_key, + &author_key, + ) + .map_err(|e| format!("open: {e}")) +} + +fn open_keyring_envelope( + trust: &Trust, + encoded: &[u8], + kind: RecipientKind, + recipient: &[u8; 16], +) -> Result { + open_envelope_against(&trust.history, encoded, kind, recipient, &trust.encryption) +} + +fn unwrap_object_key_against( + trust: &Trust, + encoded: &[u8], + object_id: [u8; 16], +) -> Result { + let (binding, author_key) = envelope_context(&trust.history, encoded, &object_id)?; + let epoch_key = trust + .keyring + .epoch_key(binding.epoch) + .ok_or("epoch key not held")?; + envelope::unwrap_object_key(encoded, &binding, epoch_key, &author_key) + .map_err(|e| format!("unwrap: {e}")) +} + +fn seal_keyring_for( + trust: &Trust, + recipient: &[u8; 16], + epoch: u64, +) -> Option, String>> { + let head = trust.head(); + if head.active_device(&trust.device_id).is_none() + || trust.keyring.epoch_key(head.epoch()).is_none() + { + return Some(Err( + "current membership and epoch key required for envelopes".into(), + )); + } + let (kind, public) = if *recipient == head.recovery_authority_id() { + (RecipientKind::Recovery, *head.recovery_encryption_key()) + } else { + let device = head.active_device(recipient)?; + (RecipientKind::Device, device.encryption_key) + }; + let Ok(public) = HpkePublicKey::from_bytes(&public) else { + return Some(Err("bad recipient key".into())); + }; + Some( + envelope::seal_keyring( + &head.envelope_binding(POLICY_OBJECT_ID, epoch.max(head.epoch()), trust.device_id), + kind, + recipient, + &public, + &trust.keyring, + &trust.signer, + ) + .map(|sealed| sealed.into_encoded()) + .map_err(|e| format!("seal: {e}")), + ) +} + +async fn fetch_all_membership(client: &VaultClient) -> Result, EngineError> { + let mut records = Vec::new(); + let mut after = -1; + loop { + let page = client.membership_after(after).await?; + let got = page.records.len() as i64; + records.extend(page.records); + if !page.truncated || got == 0 { + break; + } + after += got; + if records.len() > MAX_MEMBERSHIP_RECORDS { + return Err(EngineError::Other("membership history too long".into())); + } + } + Ok(records) +} + +fn request_from_info( + info: &super::client::EnrollmentInfo, + vault_id: [u8; 16], +) -> Option { + Some(EnrollmentRequest { + vault_id, + request_id: info.request_id.decode::<16>()?, + device_id: info.device_id.decode::<16>()?, + signing_key: info.signing_key.decode::<32>()?, + encryption_key: info.encryption_key.decode::<32>()?, + }) +} + +fn pending_request(state: &LocalVaultState, request_id: &Hex) -> Option { + let identity = identity_of(state).ok()?; + let vault = state.vault.as_ref()?; + Some(EnrollmentRequest { + vault_id: vault.vault_id.decode::<16>()?, + request_id: request_id.decode::<16>()?, + device_id: identity.device_id, + signing_key: identity.signing_key(), + encryption_key: *identity.encryption.public_key().as_bytes(), + }) +} + +fn normalize_code(code: &str) -> String { + code.chars().filter(|c| c.is_ascii_digit()).collect() +} + +fn recovery_file(state: &MembershipState) -> serde_json::Value { + serde_json::json!({ + "version": 1, + "kind": "zeron-recovery-file", + "vaultId": Hex::of(state.vault_id()).0, + "generation": Hex::of(state.generation()).0, + "genesisHash": Hex::of(state.genesis_hash()).0, + "profileHash": Hex::of(state.profile_hash()).0, + "recoveryAuthorityId": Hex::of(&state.recovery_authority_id()).0, + "createdAt": chrono::Utc::now().to_rfc3339(), + }) +} + +#[cfg(test)] +mod tests { + use super::super::store::MemoryProtection; + use super::*; + use crate::doc_host::EdgeConfig; + + fn client() -> VaultClient { + VaultClient::new( + reqwest::Client::new(), + EdgeConfig::with_static_token("http://127.0.0.1:1", "test"), + "org", + ) + } + + #[test] + fn locked_and_corrupt_stores_never_allow_plaintext() { + let dir = tempfile::tempdir().unwrap(); + let store = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + store.save(&LocalVaultState::default()).unwrap(); + let original = std::fs::read(dir.path().join("vault.json")).unwrap(); + let reopened = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + let vault = VaultService::open(reopened, Some(client()), "org", "user"); + assert!(matches!(vault.status().phase, VaultPhase::Locked { .. })); + assert!(vault.is_enrolled()); + assert_eq!( + std::fs::read(dir.path().join("vault.json")).unwrap(), + original + ); + std::fs::write(dir.path().join("vault.json"), b"invalid").unwrap(); + let reopened = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + assert!(VaultService::open(reopened, Some(client()), "org", "user").is_enrolled()); + } + + #[tokio::test] + async fn relay_stream_closes_when_encryption_becomes_required() { + use futures::StreamExt; + use zeron_rpc::{RpcReply, RpcService}; + struct Streaming; + #[async_trait::async_trait] + impl RpcService for Streaming { + async fn handle( + &self, + _: &str, + _: serde_json::Value, + ) -> Result { + Ok(RpcReply::Stream(futures::stream::pending().boxed())) + } + } + let dir = tempfile::tempdir().unwrap(); + let store = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + let vault = VaultService::open(store, Some(client()), "org", "user"); + let relay = crate::rpc::RelayRpc::new(Arc::new(Streaming), vault.clone()); + let RpcReply::Stream(mut stream) = + relay.handle("Watch", serde_json::json!({})).await.unwrap() + else { + panic!("expected stream"); + }; + lock(&vault.inner.guarded).encryption_required = true; + vault.publish_status(); + assert!( + tokio::time::timeout(std::time::Duration::from_secs(1), stream.next()) + .await + .unwrap() + .is_none() + ); + } + + #[test] + fn remote_vault_requires_encryption_before_device_approval() { + let dir = tempfile::tempdir().unwrap(); + let store = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + let vault = VaultService::open(store, Some(client()), "org", "user"); + assert!(!vault.is_enrolled()); + lock(&vault.inner.guarded).remote_vault = Some(true); + assert!(vault.is_enrolled()); + assert!(!vault.is_ready()); + } +} diff --git a/crates/engine/src/vault/store.rs b/crates/engine/src/vault/store.rs new file mode 100644 index 000000000..84b868200 --- /dev/null +++ b/crates/engine/src/vault/store.rs @@ -0,0 +1,644 @@ +//! Local persistence for one profile's vault membership: this device's +//! private identity, the pinned membership history, the keyring, and cached +//! object keys (RFC 0001 §5, §13; plan ES-04, G5). +//! +//! The state file lives under the profile store root and is encrypted with +//! AES-256-GCM under a 32-byte *protection key* the platform supplies: +//! +//! * macOS — a login-Keychain generic password (device-bound; not synced). +//! * Linux/headless — an operator-provisioned credential: a systemd +//! `$CREDENTIALS_DIRECTORY` entry or an explicit `ZERON_VAULT_KEY_FILE` +//! (0600). Both are the RFC's "unattended" mode and are opt-in by their +//! presence; without one the vault stays LOCKED. There is no silent +//! plaintext fallback. +//! * tests — an in-memory provider. +//! +//! Failure to obtain the protection key never erases the encrypted file; a +//! store that cannot be opened reports `Locked` and keeps the bytes. + +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use zeron_crypto::{CryptoError, SecretBytes}; + +use crate::EngineError; + +const STATE_FILE: &str = "vault.json"; +const FILE_VERSION: u32 = 1; +const FILE_AAD: &[u8] = b"zeron/vault-state-file/v1\0"; +const MAX_STATE_BYTES: usize = 8 * 1024 * 1024; + +/// Where the protection key came from; reported in status so the user +/// knows which credential mode protects this device's keys. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ProtectionMode { + Keychain, + SystemdCredential, + KeyFile, + Memory, +} + +/// Supplies the device-held protection key. `load_or_create` must be +/// idempotent and must never return a fresh key while one already exists +/// (that would silently orphan the encrypted state). +pub trait ProtectionKeyProvider: Send + Sync + 'static { + fn mode(&self) -> ProtectionMode; + fn load_or_create(&self, profile_key: &str) -> Result; +} + +#[derive(Debug, thiserror::Error)] +pub enum VaultStoreError { + #[error("secure key storage is unavailable: {0}")] + Locked(String), + #[error("vault state is corrupt: {0}")] + Corrupt(String), + #[error("io: {0}")] + Io(#[from] std::io::Error), + #[error("crypto: {0}")] + Crypto(CryptoError), +} + +impl From for VaultStoreError { + fn from(error: CryptoError) -> Self { + Self::Crypto(error) + } +} + +impl From for EngineError { + fn from(error: VaultStoreError) -> Self { + EngineError::Other(error.to_string()) + } +} + +/// Hex-encoded byte fields keep the JSON readable in diagnostics without +/// ever printing secrets (secret fields are wrapped in [`Secret`]). +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Hex(pub String); + +impl Hex { + pub fn of(bytes: &[u8]) -> Self { + Self(bytes.iter().map(|b| format!("{b:02x}")).collect()) + } + + pub fn decode(&self) -> Option<[u8; N]> { + let bytes = self.bytes()?; + bytes.try_into().ok() + } + + pub fn bytes(&self) -> Option> { + if !self.0.is_ascii() || !self.0.len().is_multiple_of(2) { + return None; + } + (0..self.0.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&self.0[i..i + 2], 16).ok()) + .collect() + } +} + +/// A secret byte field: serializes as hex but never appears in Debug. +#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Secret(pub Hex); + +impl Secret { + pub fn of(bytes: &[u8]) -> Self { + Self(Hex::of(bytes)) + } + pub fn decode(&self) -> Option<[u8; N]> { + self.0.decode() + } + pub fn bytes(&self) -> Option> { + self.0.bytes() + } +} + +impl std::fmt::Debug for Secret { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("[REDACTED]") + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceIdentity { + pub device_id: Hex, + pub signing_seed: Secret, + pub encryption_secret: Secret, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PinnedVault { + pub vault_id: Hex, + pub generation: Hex, + pub profile_hash: Hex, + /// Every verified policy record, base64, genesis first. + pub membership: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CachedObjectKey { + pub epoch: u64, + pub key_id: Hex, + pub key: Secret, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PendingEnrollment { + pub request_id: Hex, + pub created_at: i64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LocalVaultState { + pub version: u32, + #[serde(default)] + pub pending_membership: Option, + #[serde(default)] + pub setup_recovery: Option, + #[serde(default)] + pub pending_approval: Option<(Hex, i64)>, + #[serde(default)] + pub owed_envelope_records: std::collections::BTreeMap, + pub device: Option, + pub vault: Option, + /// Encoded keyring (`zeron_crypto::keyring::Keyring::encode`), hex. + pub keyring: Option, + /// Object id (hex) → wrapped keys this device has unwrapped. + #[serde(default)] + pub object_keys: std::collections::BTreeMap>, + pub enrollment: Option, + /// Envelopes this device still owes after a rotation it authored + /// (recipient id hex, epoch) — retried on every refresh until published. + #[serde(default)] + pub owed_envelopes: Vec<(Hex, u64)>, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct StateFile { + version: u32, + protection: ProtectionMode, + nonce: Hex, + ciphertext: Hex, +} + +/// The encrypted state file plus its protection key. +pub struct VaultStore { + path: PathBuf, + profile_key: String, + provider: Box, + protection_key: Mutex>, +} + +impl VaultStore { + pub fn new( + store_root: &Path, + profile_key: impl Into, + provider: Box, + ) -> Self { + Self { + path: store_root.join(STATE_FILE), + profile_key: profile_key.into(), + provider, + protection_key: Mutex::new(None), + } + } + + pub fn mode(&self) -> ProtectionMode { + self.provider.mode() + } + + pub fn exists(&self) -> bool { + self.path.try_exists().unwrap_or(true) + } + + fn protection_key(&self) -> Result { + let mut slot = self + .protection_key + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(key) = slot.as_ref() { + return SecretBytes::try_clone(key).map_err(VaultStoreError::from); + } + let key = self.provider.load_or_create(&self.profile_key)?; + if key.as_bytes().len() != 32 { + return Err(VaultStoreError::Locked( + "protection key has the wrong length".into(), + )); + } + let clone = SecretBytes::try_clone(&key)?; + *slot = Some(key); + Ok(clone) + } + + /// Load the state, or an empty state when no file exists yet. + pub fn load(&self) -> Result { + let bytes = match std::fs::read(&self.path) { + Ok(bytes) => bytes, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // Touching the protection key on first use surfaces a + // locked store before any secret is generated. + self.protection_key()?; + return Ok(LocalVaultState { + version: FILE_VERSION, + ..Default::default() + }); + } + Err(err) => return Err(err.into()), + }; + if bytes.len() > MAX_STATE_BYTES { + return Err(VaultStoreError::Corrupt("state file too large".into())); + } + let file: StateFile = serde_json::from_slice(&bytes) + .map_err(|err| VaultStoreError::Corrupt(format!("state file: {err}")))?; + if file.version != FILE_VERSION { + return Err(VaultStoreError::Corrupt(format!( + "unsupported vault state version {}", + file.version + ))); + } + let key = self.protection_key()?; + let nonce: [u8; 12] = file + .nonce + .decode() + .ok_or_else(|| VaultStoreError::Corrupt("bad nonce".into()))?; + let ciphertext = file + .ciphertext + .bytes() + .ok_or_else(|| VaultStoreError::Corrupt("bad ciphertext".into()))?; + let plaintext = zeron_crypto::open_aes256_gcm( + key.as_bytes(), + &nonce, + FILE_AAD, + &ciphertext, + MAX_STATE_BYTES, + ) + .map_err(|_| { + VaultStoreError::Locked( + "vault state does not decrypt under this device's protection key".into(), + ) + })?; + let state: LocalVaultState = serde_json::from_slice(plaintext.as_bytes()) + .map_err(|err| VaultStoreError::Corrupt(format!("state payload: {err}")))?; + Ok(state) + } + + /// Encrypt and atomically replace the state file (0600, fsync, rename). + pub fn save(&self, state: &LocalVaultState) -> Result<(), VaultStoreError> { + let key = self.protection_key()?; + let plaintext = serde_json::to_vec(state) + .map_err(|err| VaultStoreError::Corrupt(format!("serialize: {err}")))?; + let mut nonce = [0u8; 12]; + zeron_crypto::fill_random(&mut nonce)?; + let ciphertext = + zeron_crypto::seal_aes256_gcm(key.as_bytes(), &nonce, FILE_AAD, &plaintext)?; + let file = StateFile { + version: FILE_VERSION, + protection: self.provider.mode(), + nonce: Hex::of(&nonce), + ciphertext: Hex::of(&ciphertext), + }; + let bytes = serde_json::to_vec_pretty(&file) + .map_err(|err| VaultStoreError::Corrupt(format!("serialize: {err}")))?; + if bytes.len() > MAX_STATE_BYTES { + return Err(VaultStoreError::Corrupt("state file too large".into())); + } + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent)?; + } + let temp = self + .path + .with_extension(format!("tmp-{}", uuid::Uuid::new_v4())); + write_private(&temp, &bytes)?; + std::fs::rename(&temp, &self.path)?; + #[cfg(unix)] + if let Some(parent) = self.path.parent() { + std::fs::File::open(parent)?.sync_all()?; + } + Ok(()) + } +} + +fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> { + use std::io::Write; + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(path)?; + #[cfg(unix)] + file.set_permissions(std::os::unix::fs::PermissionsExt::from_mode(0o600))?; + file.write_all(bytes)?; + file.sync_all()?; + Ok(()) +} + +// ── providers ─────────────────────────────────────────────────────────────── + +/// Test-only: a random key held in memory for the process lifetime. +pub struct MemoryProtection(Mutex>>); + +impl MemoryProtection { + pub fn new() -> Self { + Self(Mutex::new(None)) + } +} + +impl Default for MemoryProtection { + fn default() -> Self { + Self::new() + } +} + +impl ProtectionKeyProvider for MemoryProtection { + fn mode(&self) -> ProtectionMode { + ProtectionMode::Memory + } + + fn load_or_create(&self, _profile_key: &str) -> Result { + let mut slot = self + .0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if slot.is_none() { + let mut key = vec![0u8; 32]; + zeron_crypto::fill_random(&mut key)?; + *slot = Some(key); + } + Ok(SecretBytes::from_slice(slot.as_ref().expect("initialized"))) + } +} + +/// Always locked: the platform offered no secure store. Keeps the encrypted +/// file untouched and reports why. +pub struct LockedProtection(pub String); + +impl ProtectionKeyProvider for LockedProtection { + fn mode(&self) -> ProtectionMode { + ProtectionMode::Memory + } + + fn load_or_create(&self, _profile_key: &str) -> Result { + Err(VaultStoreError::Locked(self.0.clone())) + } +} + +/// Operator-provisioned key file (unattended mode): 64 hex chars or 32 raw +/// bytes, must be a regular file with mode 0600 owned by this process. +pub struct KeyFileProtection { + path: PathBuf, + mode: ProtectionMode, +} + +impl KeyFileProtection { + pub fn new(path: PathBuf, mode: ProtectionMode) -> Self { + Self { path, mode } + } +} + +impl ProtectionKeyProvider for KeyFileProtection { + fn mode(&self) -> ProtectionMode { + self.mode + } + + fn load_or_create(&self, _profile_key: &str) -> Result { + let metadata = std::fs::metadata(&self.path).map_err(|err| { + VaultStoreError::Locked(format!( + "vault key file {} is not readable: {err}", + self.path.display() + )) + })?; + if !metadata.is_file() { + return Err(VaultStoreError::Locked(format!( + "vault key file {} is not a regular file", + self.path.display() + ))); + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if metadata.permissions().mode() & 0o077 != 0 { + return Err(VaultStoreError::Locked(format!( + "vault key file {} must not be group/world accessible", + self.path.display() + ))); + } + } + let raw = std::fs::read(&self.path)?; + let key = if raw.len() == 32 { + raw + } else { + let text = String::from_utf8_lossy(&raw); + Hex(text.trim().to_string()) + .bytes() + .filter(|k| k.len() == 32) + .ok_or_else(|| { + VaultStoreError::Locked(format!( + "vault key file {} must hold 32 raw bytes or 64 hex characters", + self.path.display() + )) + })? + }; + Ok(SecretBytes::from_slice(&key)) + } +} + +/// macOS login Keychain generic password via the `security` CLI (the same +/// primitive `agent_accounts` uses). Item service `sh.zeron.vault`, account +/// = the profile key, so each profile has its own device-bound key. +#[cfg(target_os = "macos")] +pub struct KeychainProtection; + +#[cfg(target_os = "macos")] +impl ProtectionKeyProvider for KeychainProtection { + fn mode(&self) -> ProtectionMode { + ProtectionMode::Keychain + } + + fn load_or_create(&self, profile_key: &str) -> Result { + const SERVICE: &str = "sh.zeron.vault"; + let run = |args: &[&str]| -> Result<(bool, String), VaultStoreError> { + let output = std::process::Command::new("security") + .args(args) + .stdin(std::process::Stdio::null()) + .output() + .map_err(|err| VaultStoreError::Locked(format!("security: {err}")))?; + Ok(( + output.status.success(), + String::from_utf8_lossy(&output.stdout).trim().to_string(), + )) + }; + let (found, existing) = run(&[ + "find-generic-password", + "-a", + profile_key, + "-s", + SERVICE, + "-w", + ])?; + if found { + return Hex(existing) + .bytes() + .filter(|k| k.len() == 32) + .map(|k| SecretBytes::from_slice(&k)) + .ok_or_else(|| { + VaultStoreError::Locked("keychain item is not a 32-byte key".into()) + }); + } + let mut key = [0u8; 32]; + zeron_crypto::fill_random(&mut key)?; + let hex = Hex::of(&key).0; + // No `-U`: never overwrite an item that appeared between the probe + // and the add (a second engine racing us) — re-read instead. + let (added, _) = run(&[ + "add-generic-password", + "-a", + profile_key, + "-s", + SERVICE, + "-w", + &hex, + ])?; + if !added { + let (found, existing) = run(&[ + "find-generic-password", + "-a", + profile_key, + "-s", + SERVICE, + "-w", + ])?; + if found { + return Hex(existing) + .bytes() + .filter(|k| k.len() == 32) + .map(|k| SecretBytes::from_slice(&k)) + .ok_or_else(|| { + VaultStoreError::Locked("keychain item is not a 32-byte key".into()) + }); + } + return Err(VaultStoreError::Locked( + "macOS Keychain refused to store the vault protection key".into(), + )); + } + Ok(SecretBytes::from_slice(&key)) + } +} + +/// Pick the platform provider. Explicit credentials win (unattended mode), +/// then the macOS Keychain; anything else stays locked. +pub fn platform_protection() -> Box { + if let Some(path) = std::env::var_os("ZERON_VAULT_KEY_FILE").filter(|p| !p.is_empty()) { + return Box::new(KeyFileProtection::new( + PathBuf::from(path), + ProtectionMode::KeyFile, + )); + } + if let Some(dir) = std::env::var_os("CREDENTIALS_DIRECTORY").filter(|p| !p.is_empty()) { + let path = PathBuf::from(dir).join("zeron-vault-key"); + if path.exists() { + return Box::new(KeyFileProtection::new( + path, + ProtectionMode::SystemdCredential, + )); + } + } + #[cfg(target_os = "macos")] + { + Box::new(KeychainProtection) + } + #[cfg(not(target_os = "macos"))] + { + Box::new(LockedProtection( + "no secure key store: provide ZERON_VAULT_KEY_FILE or a systemd credential \ + named zeron-vault-key (unattended mode)" + .into(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn state_round_trips_encrypted_and_stays_sealed_without_the_key() { + let dir = tempfile::tempdir().unwrap(); + let store = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + assert_eq!(store.load().unwrap().device, None); + let state = LocalVaultState { + version: FILE_VERSION, + device: Some(DeviceIdentity { + device_id: Hex::of(&[1; 16]), + signing_seed: Secret::of(&[2; 32]), + encryption_secret: Secret::of(&[3; 32]), + }), + ..Default::default() + }; + store.save(&state).unwrap(); + assert_eq!(store.load().unwrap(), state); + let raw = std::fs::read_to_string(dir.path().join(STATE_FILE)).unwrap(); + assert!( + !raw.contains(&Hex::of(&[2; 32]).0), + "secrets never appear in the file" + ); + // A different protection key cannot open the file and does not erase it. + let other = VaultStore::new(dir.path(), "org/user", Box::new(MemoryProtection::new())); + assert!(matches!(other.load(), Err(VaultStoreError::Locked(_)))); + assert!(dir.path().join(STATE_FILE).exists()); + // A locked provider reports locked without touching the file. + let locked = VaultStore::new( + dir.path(), + "org/user", + Box::new(LockedProtection("no store".into())), + ); + assert!(matches!(locked.load(), Err(VaultStoreError::Locked(_)))); + assert_eq!( + format!("{:?}", state.device.as_ref().unwrap().signing_seed), + "[REDACTED]" + ); + } + + #[test] + fn malformed_hex_is_rejected_without_panicking() { + for value in ["aéa", "xyz", "gg", "00ffz0"] { + assert!(Hex(value.into()).bytes().is_none()); + } + } + + #[test] + fn key_file_provider_enforces_permissions_and_format() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("key"); + std::fs::write(&path, Hex::of(&[7; 32]).0).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap(); + let provider = KeyFileProtection::new(path.clone(), ProtectionMode::KeyFile); + assert!(matches!( + provider.load_or_create("p"), + Err(VaultStoreError::Locked(_)) + )); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); + } + let provider = KeyFileProtection::new(path.clone(), ProtectionMode::KeyFile); + assert_eq!(provider.load_or_create("p").unwrap().as_bytes(), &[7; 32]); + std::fs::write(&path, "not a key").unwrap(); + assert!(matches!( + provider.load_or_create("p"), + Err(VaultStoreError::Locked(_)) + )); + } +} diff --git a/crates/engine/src/workspace_host.rs b/crates/engine/src/workspace_host.rs index f3154b2a5..848016dda 100644 --- a/crates/engine/src/workspace_host.rs +++ b/crates/engine/src/workspace_host.rs @@ -145,6 +145,10 @@ pub struct WorkspaceHostConfig { /// The signed-in user — registries are per-user (`reg1/{orgId}/{userId}`): /// spaces/sessions are private to their owner, never org-visible. pub user_id: String, + /// Encrypted-sync vault: when the profile is enrolled, the registry joins + /// its ENCRYPTED generation (`/registry/{orgId}/e1/ws`) with per-field + /// sealing and never the plaintext room (RFC 0001 §9). + pub vault: Option, /// When present, the host joins `/registry/{orgId}/ws`. `None` = fully offline /// (local snapshots only; the registry still drives everything device-side). pub edge: Option, @@ -159,6 +163,7 @@ struct WorkspaceHostInner { sessions_tx: watch::Sender>, spaces_tx: watch::Sender>, room: Mutex>>, + join_task: Mutex>>, /// Bumped on every registry change (local mutation or applied server /// frame) — drives republish + the snapshot debounce in `workspace_task`. changed_tx: watch::Sender, @@ -243,6 +248,9 @@ impl WorkspaceHost { doc } }; + if config.vault.as_ref().is_some_and(|v| v.is_enrolled()) { + doc.enter_encrypted_room(); + } // Destructive-break hygiene: the pre-spaces row stays unreachable. store.delete_snapshot(LEGACY_WORKSPACE_DOC_ID).ok(); @@ -255,6 +263,7 @@ impl WorkspaceHost { .into_iter() .find(|d| d.id == config.device_id); doc.upsert_device(&Device { + vault_device_id: config.vault.as_ref().and_then(|v| v.status().device_id), id: config.device_id.clone(), name: device_name_on_boot( existing.as_ref().map(|device| device.name.as_str()), @@ -288,6 +297,7 @@ impl WorkspaceHost { sessions_tx, spaces_tx, room: Mutex::new(None), + join_task: Mutex::new(None), changed_tx, presence_seen: Mutex::new(std::collections::HashMap::new()), peer_alive: Mutex::new(None), @@ -300,6 +310,42 @@ impl WorkspaceHost { // dies before the first debounced save. host.inner.save_snapshot(); host.join_room(); + if let Some(vault) = host.inner.config.vault.clone().filter(|v| !v.is_enrolled()) { + let weak = Arc::downgrade(&host.inner); + let mut status = vault.watch_status(); + tokio::spawn(async move { + loop { + if vault.is_enrolled() { + let Some(inner) = weak.upgrade() else { return }; + // Stop the old generation before resetting its independent cursor. + let previous = lock(&inner.join_task).take(); + if let Some(task) = previous { + task.abort(); + let _ = task.await; + } + lock(&inner.room).take(); + { + let mut doc = lock(&inner.reg); + doc.enter_encrypted_room(); + if let Ok(Some(mut device)) = doc.read_devices().map(|devices| { + devices.into_iter().find(|d| d.id == inner.config.device_id) + }) { + device.vault_device_id = vault.status().device_id; + let _ = doc.upsert_device(&device); + } + } + inner.save_snapshot(); + inner.publish(); + inner.bump_changed(); + WorkspaceHost { inner }.join_room(); + return; + } + if status.changed().await.is_err() || weak.upgrade().is_none() { + return; + } + } + }); + } tokio::spawn(workspace_task(Arc::downgrade(&host.inner), changed_rx)); if host.inner.config.edge.is_some() { tokio::spawn(relay_probe_task(Arc::downgrade(&host.inner))); @@ -313,9 +359,26 @@ impl WorkspaceHost { return; }; let org_id = self.inner.config.org_id.clone(); + let vault = self + .inner + .config + .vault + .clone() + .filter(|vault| vault.is_enrolled()); // Per-dial URL provider: the bearer is re-read on every (re)connect. - let url = edge.room_url(format!("/registry/{org_id}/ws")); - self.spawn_join(url, edge.token_changes(), Some(edge.token.clone())); + // Encrypted profiles address the encrypted registry generation and + // seal every value; the plaintext room is never joined again. + let (url, codec) = match vault { + Some(vault) => ( + edge.room_url(format!("/registry/{org_id}/e1/ws")), + Some(Arc::new(crate::vault::VaultRegistryCodec::new( + vault, + &self.inner.config.user_id, + ))), + ), + None => (edge.room_url(format!("/registry/{org_id}/ws")), None), + }; + self.spawn_join(url, edge.token_changes(), Some(edge.token.clone()), codec); } /// Test seam: join a registry room at a fixed WebSocket URL without an @@ -323,7 +386,12 @@ impl WorkspaceHost { /// server through this. Production always goes through [`Self::join_room`]. #[doc(hidden)] pub fn connect_registry_url(&self, url: &str) { - self.spawn_join(Arc::new(zeron_sync::StaticUrl(url.to_string())), None, None); + self.spawn_join( + Arc::new(zeron_sync::StaticUrl(url.to_string())), + None, + None, + None, + ); } fn spawn_join( @@ -331,12 +399,13 @@ impl WorkspaceHost { url: Arc, mut token_changes: Option>, token: Option>, + codec: Option>, ) { let org_id = self.inner.config.org_id.clone(); let reg = self.inner.reg.clone(); let device_id = self.inner.config.device_id.clone(); let weak = Arc::downgrade(&self.inner); - tokio::spawn(async move { + let task = tokio::spawn(async move { let mut wake = zeron_sync::wake::subscribe(); let mut online = zeron_sync::wake::subscribe_online(); // `RegistryClient` only self-reconnects AFTER a first successful @@ -352,12 +421,48 @@ impl WorkspaceHost { let tuning = RegistryTuning { probe_quiet: REGISTRY_PROBE_QUIET, }; + // Vault gate (RFC 0001 §4.3): an encrypted registry is joined + // only with usable keys — locked / key-update / revoked wait + // here, and sealing material is prepared before the first + // push so no batch ever leaves unsealed. + if let Some(codec) = &codec { + let vault = codec.vault().clone(); + let mut status = vault.watch_status(); + let mut announced = false; + loop { + if vault.is_ready() && codec.prepare().await.is_ok() { + break; + } + if !announced { + tracing::info!(status = ?vault.status().phase, + "registry: encrypted join waiting for the vault"); + announced = true; + let vault = vault.clone(); + tokio::spawn(async move { + let _ = vault.refresh().await; + }); + } + if status.changed().await.is_err() || weak.upgrade().is_none() { + return; + } + } + } // Production path (token present): dual transport — the WS // dials as before, and a plain-HTTPS pull/push seam derived // from the same URL provider bootstraps the doc in ~1 RTT // and keeps syncing at backoff cadence when the socket can't // connect (airplane-wifi networks strip WS upgrades). - let result = if token.is_some() { + let result = if let (Some(codec), true) = (&codec, token.is_some()) { + RegistryClient::connect_via_transport_with_codec( + url.clone(), + reg.clone(), + &device_id, + tuning, + Arc::new(WsDerivedRegistryTransport::new(url.clone())), + codec.clone(), + ) + .await + } else if token.is_some() { RegistryClient::connect_via_transport( url.clone(), reg.clone(), @@ -458,6 +563,9 @@ impl WorkspaceHost { } } }); + if let Some(previous) = lock(&self.inner.join_task).replace(task) { + previous.abort(); + } } /// Close the current registry membership before account-scoped state is @@ -1049,6 +1157,14 @@ impl WorkspaceHost { Ok(self.mutate(|doc| doc.delete_chat(chat_id))?) } + pub fn vault_device_names(&self) -> std::collections::BTreeMap { + lock(&self.inner.reg).vault_device_names() + } + + pub fn rename_vault_device(&self, id: &str, name: &str) -> Result<(), EngineError> { + Ok(self.mutate(|doc| doc.rename_vault_device(id, name))?) + } + pub fn rename_device(&self, device_id: &str, name: &str) -> Result { Ok(self.mutate(|doc| doc.rename_device(device_id, name))?) } @@ -1582,6 +1698,7 @@ mod tests { org_id: "test-org".into(), user_id: "test-user".into(), edge: None, + vault: None, }, ) .unwrap(); diff --git a/crates/engine/tests/e2e.rs b/crates/engine/tests/e2e.rs index 389bbc823..00edda560 100644 --- a/crates/engine/tests/e2e.rs +++ b/crates/engine/tests/e2e.rs @@ -806,7 +806,8 @@ async fn rpc_surface_over_in_memory_transport() { .await .unwrap() .unwrap(); - // Delta protocol: the stream opens with a full reset frame. + // Delta protocol: the stream opens with a full reset frame (plus the + // context-usage field every transcript update carries since #264). assert_eq!( initial, serde_json::json!({ "reset": [], "contextUsage": null }) @@ -1855,11 +1856,9 @@ async fn empty_reasoning_deltas_are_heartbeats_not_journal_noise() { ); wait_for( || { - entries(&core) - .iter() - .any(|e| { - e.role == MessageRole::Assistant && e.status == Some(MessageStatus::Complete) - }) + entries(&core).iter().any(|entry| { + entry.role == MessageRole::Assistant && entry.status == Some(MessageStatus::Complete) + }) }, "run completes", ) diff --git a/crates/engine/tests/relay_delivery.rs b/crates/engine/tests/relay_delivery.rs index 9411b15a4..74ac233d1 100644 --- a/crates/engine/tests/relay_delivery.rs +++ b/crates/engine/tests/relay_delivery.rs @@ -218,6 +218,7 @@ async fn rows_dark_command_delivers_over_the_peer_relay_exactly_once() { // A knows the chat is hosted on B (local registry writes), and knows B's // stamped version passes the relay gate. core_a.workspace.upsert_device_row(&Device { + vault_device_id: None, id: "device-b".into(), name: "b".into(), platform: "linux".into(), diff --git a/crates/engine/tests/vault_crash_recovery.rs b/crates/engine/tests/vault_crash_recovery.rs new file mode 100644 index 000000000..08e573a0e --- /dev/null +++ b/crates/engine/tests/vault_crash_recovery.rs @@ -0,0 +1,328 @@ +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use zeron_crypto::{SecretBytes, keyring::Keyring, record::UnverifiedRecord}; +use zeron_engine::doc_host::EdgeConfig; +use zeron_engine::vault::client::{VaultClient, encode_base64}; +use zeron_engine::vault::store::{LocalVaultState, VaultStoreError}; +use zeron_engine::vault::{ + ProtectionKeyProvider, ProtectionMode, VaultPhase, VaultService, VaultStore, +}; + +struct Protection; + +impl ProtectionKeyProvider for Protection { + fn mode(&self) -> ProtectionMode { + ProtectionMode::Memory + } + fn load_or_create(&self, _: &str) -> Result { + Ok(SecretBytes::from_slice(&[42; 32])) + } +} + +fn store(path: &Path) -> VaultStore { + VaultStore::new(path, "org/user", Box::new(Protection)) +} + +struct ServerState { + path: PathBuf, + records: Vec>, + attempts: Vec>, + journal_observed: Vec, + envelope_attempts: Vec<(String, Vec)>, + accept_membership: bool, + accept_envelopes: bool, + lose_reply: bool, +} + +impl ServerState { + fn response(&mut self, method: &str, path: &str, body: &[u8]) -> (u16, serde_json::Value) { + if method == "POST" && path == "/vault/org/membership" { + let local = store(&self.path).load().unwrap(); + let epoch = UnverifiedRecord::parse(body, 65536) + .unwrap() + .untrusted_binding() + .epoch; + let held = local + .keyring + .as_ref() + .and_then(|key| key.bytes()) + .and_then(|bytes| Keyring::decode(&bytes).ok()) + .is_some_and(|keyring| keyring.epoch_key(epoch).is_some()); + self.journal_observed.push( + local.pending_membership.as_deref() == Some(encode_base64(body).as_str()) && held, + ); + self.attempts.push(body.to_vec()); + if !self.accept_membership { + return ( + 503, + serde_json::json!({"ok": false, "error": "unavailable"}), + ); + } + if self.records.last().is_some_and(|last| last == body) { + return ( + 409, + serde_json::json!({"ok": false, "error": "stale_parent"}), + ); + } + self.records.push(body.to_vec()); + if self.lose_reply { + self.lose_reply = false; + return (503, serde_json::json!({"ok": false, "error": "lost_ack"})); + } + return (200, serde_json::json!({"ok": true})); + } + if method == "GET" && path.starts_with("/vault/org/membership?") { + let after: i64 = path.split("after=").nth(1).unwrap().parse().unwrap(); + let records: Vec<_> = self + .records + .iter() + .skip((after + 1) as usize) + .map(|r| encode_base64(r)) + .collect(); + return ( + 200, + serde_json::json!({"records": records, "headSeq": self.records.len() as i64 - 1, "truncated": false}), + ); + } + if method == "PUT" && path.starts_with("/vault/org/envelopes/") { + self.envelope_attempts + .push((path.to_string(), body.to_vec())); + return if self.accept_envelopes { + (200, serde_json::json!({"ok": true})) + } else { + ( + 503, + serde_json::json!({"ok": false, "error": "unavailable"}), + ) + }; + } + (404, serde_json::json!({"error": "not_found"})) + } +} + +struct Server { + url: String, + state: Arc>, + task: tokio::task::JoinHandle<()>, +} + +impl Server { + async fn start(path: &Path) -> Self { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let state = Arc::new(Mutex::new(ServerState { + path: path.to_path_buf(), + records: Vec::new(), + attempts: Vec::new(), + journal_observed: Vec::new(), + envelope_attempts: Vec::new(), + accept_membership: true, + accept_envelopes: true, + lose_reply: false, + })); + let shared = state.clone(); + let task = tokio::spawn(async move { + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + let mut bytes = Vec::new(); + let mut buffer = [0; 4096]; + let header_end = loop { + let n = socket.read(&mut buffer).await.unwrap(); + assert!(n > 0); + bytes.extend_from_slice(&buffer[..n]); + assert!(bytes.len() <= 128 * 1024); + if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") { + break end + 4; + } + }; + let headers = String::from_utf8(bytes[..header_end].to_vec()).unwrap(); + let mut request = headers.lines().next().unwrap().split_whitespace(); + let method = request.next().unwrap(); + let path = request.next().unwrap(); + let length = headers + .lines() + .filter_map(|line| line.split_once(':')) + .find(|(name, _)| name.eq_ignore_ascii_case("content-length")) + .map(|(_, value)| value.trim().parse::().unwrap()) + .unwrap_or(0); + assert!(length <= 128 * 1024); + while bytes.len() < header_end + length { + let n = socket.read(&mut buffer).await.unwrap(); + assert!(n > 0); + bytes.extend_from_slice(&buffer[..n]); + } + let (status, body) = shared.lock().unwrap().response( + method, + path, + &bytes[header_end..header_end + length], + ); + let body = body.to_string(); + let response = format!( + "HTTP/1.1 {status} Test\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + socket.write_all(response.as_bytes()).await.unwrap(); + } + }); + Self { url, state, task } + } + + fn vault(&self, path: &Path) -> VaultService { + let http = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(3)) + .build() + .unwrap(); + let client = VaultClient::new( + http, + EdgeConfig::with_static_token(&self.url, "test"), + "org", + ); + VaultService::open(store(path), Some(client), "org", "user") + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.task.abort(); + } +} + +#[tokio::test] +async fn setup_restarts_with_identical_membership_and_requires_kit_confirmation() { + let dir = tempfile::tempdir().unwrap(); + let server = Server::start(dir.path()).await; + server.state.lock().unwrap().accept_membership = false; + let vault = server.vault(dir.path()); + assert!(vault.setup().await.is_err()); + assert!(vault.is_enrolled()); + assert!(!vault.is_ready()); + let staged = store(dir.path()).load().unwrap(); + assert!(staged.pending_membership.is_some()); + assert!(staged.setup_recovery.is_some()); + drop(vault); + server.state.lock().unwrap().accept_membership = true; + let vault = server.vault(dir.path()); + let kit = vault.setup().await.unwrap(); + assert_eq!( + vault.status().phase, + VaultPhase::RecoveryConfirmationRequired + ); + assert!(!vault.is_ready()); + assert!(vault.seal_material([9; 16]).await.is_err()); + assert_eq!(vault.setup().await.unwrap().kit, kit.kit); + { + let state = server.state.lock().unwrap(); + assert_eq!(state.attempts[0], state.attempts[1]); + assert!(state.journal_observed.iter().all(|seen| *seen)); + } + vault.confirm_recovery_kit().await.unwrap(); + assert!(vault.is_ready()); + let saved = store(dir.path()).load().unwrap(); + assert!(saved.setup_recovery.is_none()); + assert!(saved.pending_membership.is_none()); + assert!(saved.owed_envelopes.is_empty()); + assert!(vault.setup().await.is_err()); +} + +#[tokio::test] +async fn accepted_membership_and_failed_envelopes_survive_restart() { + let dir = tempfile::tempdir().unwrap(); + let server = Server::start(dir.path()).await; + { + let mut state = server.state.lock().unwrap(); + state.lose_reply = true; + state.accept_envelopes = false; + } + let vault = server.vault(dir.path()); + assert!(vault.setup().await.is_err()); + let before: LocalVaultState = store(dir.path()).load().unwrap(); + assert!(before.pending_membership.is_none()); + assert_eq!(before.owed_envelopes.len(), 2); + assert!(!vault.is_ready()); + drop(vault); + server.state.lock().unwrap().accept_envelopes = true; + let vault = server.vault(dir.path()); + let kit = vault.setup().await.unwrap(); + assert!(!kit.kit.is_empty()); + assert_eq!(server.state.lock().unwrap().records.len(), 1); + assert_eq!(store(dir.path()).load().unwrap().keyring, before.keyring); + vault.confirm_recovery_kit().await.unwrap(); + assert!(server.vault(dir.path()).is_ready()); + let state = server.state.lock().unwrap(); + assert_eq!(state.envelope_attempts.len(), 4); + assert_eq!(state.envelope_attempts[0], state.envelope_attempts[2]); + assert_eq!(state.envelope_attempts[1], state.envelope_attempts[3]); +} + +#[tokio::test] +async fn rotation_key_is_durable_before_publication_and_survives_restart() { + use zeron_crypto::{ + content::DeviceSigner, + hpke::HpkePrivateKey, + policy::{self, DeviceEntry, DeviceStatus, MembershipState, Operation}, + }; + use zeron_engine::vault::store::Hex; + let dir = tempfile::tempdir().unwrap(); + let server = Server::start(dir.path()).await; + let vault = server.vault(dir.path()); + vault.setup().await.unwrap(); + vault.confirm_recovery_kit().await.unwrap(); + let local = store(dir.path()).load().unwrap(); + let pin = local.vault.unwrap(); + let identity = local.device.unwrap(); + let genesis = server.state.lock().unwrap().records[0].clone(); + let head = MembershipState::from_genesis( + &genesis, + &pin.vault_id.decode().unwrap(), + &pin.generation.decode().unwrap(), + &policy::profile_hash("org", "user"), + ) + .unwrap(); + let signer = DeviceSigner::from_seed( + identity.device_id.decode().unwrap(), + &identity.signing_seed.bytes().unwrap(), + ) + .unwrap(); + let peer_id = [9; 16]; + let peer = DeviceSigner::from_seed(peer_id, &[7; 32]).unwrap(); + let encryption = HpkePrivateKey::from_bytes(&[8; 32]).unwrap(); + let mut payload = head.next_payload(Operation::AddDevice); + payload.devices.push(DeviceEntry { + device_id: peer_id, + signing_key: peer.public_key().try_into().unwrap(), + encryption_key: *encryption.public_key().as_bytes(), + status: DeviceStatus::Active, + }); + let binding = policy::policy_binding( + *head.vault_id(), + *head.generation(), + payload.epoch, + *signer.author_id(), + *head.hash(), + ); + let added = policy::encode_policy(&binding, &payload, &signer).unwrap(); + server.state.lock().unwrap().records.push(added); + vault.refresh().await.unwrap(); + server.state.lock().unwrap().accept_membership = false; + assert!(vault.revoke(&Hex::of(&peer_id).0).await.is_err()); + let staged = store(dir.path()).load().unwrap(); + assert!(staged.pending_membership.is_some()); + let ring = Keyring::decode(&staged.keyring.unwrap().bytes().unwrap()).unwrap(); + let key = ring.epoch_key(2).unwrap().to_vec(); + assert!(!vault.is_ready()); + drop(vault); + server.state.lock().unwrap().accept_membership = true; + let vault = server.vault(dir.path()); + vault.refresh().await.unwrap(); + assert!(vault.is_ready()); + assert_eq!(vault.status().epoch, Some(2)); + let saved = store(dir.path()).load().unwrap(); + assert!(saved.pending_membership.is_none()); + let ring = Keyring::decode(&saved.keyring.unwrap().bytes().unwrap()).unwrap(); + assert_eq!(ring.epoch_key(2).unwrap().as_slice(), key.as_slice()); + let state = server.state.lock().unwrap(); + assert!(state.journal_observed.iter().all(|seen| *seen)); + assert_eq!(state.attempts[1], state.attempts[2]); +} diff --git a/crates/engine/tests/vault_e2e.rs b/crates/engine/tests/vault_e2e.rs new file mode 100644 index 000000000..d20139f0d --- /dev/null +++ b/crates/engine/tests/vault_e2e.rs @@ -0,0 +1,903 @@ +//! End-to-end vault control-plane exercise against a REAL edge worker +//! (`wrangler dev --var AUTH_MODE:dev`): two devices set up / pair / seal / +//! open / revoke / rotate, and a third recovers with the kit. The edge holds +//! only ciphertext and public records throughout; every trust decision is +//! made client-side against local pins. +//! +//! Run with: +//! (cd edge && npx wrangler dev --port 27640 --var AUTH_MODE:dev --local) +//! ZERON_VAULT_EDGE_URL=http://127.0.0.1:27640 cargo test -p zeron-engine --test vault_e2e +//! +//! Without the env var the test is skipped (no network in unit CI). + +use std::sync::Arc; + +use zeron_crypto::content::{self, ContentPurpose}; +use zeron_crypto::record::UnverifiedRecord; +use zeron_engine::doc_host::EdgeConfig; +use zeron_engine::vault::client::VaultClient; +use zeron_engine::vault::{MemoryProtection, VaultPhase, VaultService, VaultStore, object_id_for}; + +fn edge_url() -> Option { + std::env::var("ZERON_VAULT_EDGE_URL") + .ok() + .filter(|u| !u.trim().is_empty()) +} + +fn device(dir: &std::path::Path, edge: &str, org: &str, user: &str) -> VaultService { + let bearer = format!("{user}@{org}"); + let config = EdgeConfig::with_static_token(edge, bearer); + let client = VaultClient::new(reqwest::Client::new(), config, org); + let store = VaultStore::new( + dir, + format!("{org}/{user}"), + Box::new(MemoryProtection::new()), + ); + VaultService::open(store, Some(client), org, user) +} + +fn fresh_profile() -> (String, String) { + let nonce = uuid::Uuid::new_v4().simple().to_string(); + ( + format!("org-{}", &nonce[..8]), + format!("user-{}", &nonce[8..16]), + ) +} + +#[tokio::test] +async fn two_devices_pair_seal_open_revoke_and_recover() { + let Some(edge) = edge_url() else { + eprintln!("ZERON_VAULT_EDGE_URL unset; skipping live vault e2e"); + return; + }; + let (org, user) = fresh_profile(); + let dir_a = tempfile::tempdir().unwrap(); + let dir_b = tempfile::tempdir().unwrap(); + let dir_c = tempfile::tempdir().unwrap(); + + // ── A: nothing exists yet, set up ──────────────────────────────────── + let a = device(dir_a.path(), &edge, &org, &user); + let status = a.refresh().await.unwrap(); + assert_eq!( + status.phase, + VaultPhase::NotEnrolled { + remote_vault: false + } + ); + let kit = a.setup().await.unwrap(); + assert_eq!(a.status().phase, VaultPhase::RecoveryConfirmationRequired); + a.confirm_recovery_kit().await.unwrap(); + assert!(a.is_ready(), "{:?}", a.status().phase); + assert_eq!(kit.kit.split('-').count(), 11); + assert!(a.setup().await.is_err(), "second setup is refused"); + + // ── B: pairs through the untrusted relay with a comparison code ───── + let b = device(dir_b.path(), &edge, &org, &user); + let status = b.refresh().await.unwrap(); + assert_eq!(status.phase, VaultPhase::NotEnrolled { remote_vault: true }); + assert!( + b.setup().await.is_err(), + "cannot create a second vault over an existing one" + ); + let (request_id, code_on_b) = b.request_enrollment().await.unwrap(); + assert!(matches!(b.status().phase, VaultPhase::Pending { .. })); + let pending = a.pending_requests().await.unwrap(); + let request = pending + .iter() + .find(|r| r.request_id == request_id) + .expect("A sees B's request"); + assert_eq!( + request.pairing_code, code_on_b, + "both sides derive the same code" + ); + // A wrong code (a relay that swapped keys) is refused. + assert!(a.approve(&request_id, "0000-0000").await.is_err()); + a.approve(&request_id, &code_on_b).await.unwrap(); + // B learns of the approval on refresh and becomes Ready. + let status = b.refresh().await.unwrap(); + assert_eq!(status.phase, VaultPhase::Ready, "{status:?}"); + assert_eq!(status.devices.len(), 2); + + // ── A seals, B opens (object key published through the control plane) + let object = object_id_for("chat", "chat-e2e"); + let material = a.seal_material(object).await.unwrap(); + let sealed = content::seal( + &material.binding, + ContentPurpose::ChatUpdate, + &material.key, + &material.signer, + b"private canary from A", + 1024, + ) + .unwrap(); + let untrusted = *UnverifiedRecord::parse(sealed.encoded(), 2048) + .unwrap() + .untrusted_binding(); + let context = b.open_material(object, &untrusted).await.unwrap(); + let opened = content::open( + sealed.encoded(), + &context.binding, + ContentPurpose::ChatUpdate, + &context.key, + &context.author_public_key, + 1024, + ) + .unwrap(); + assert_eq!(opened.plaintext().as_bytes(), b"private canary from A"); + // Both writers converge on ONE key per object/epoch (first writer wins). + let material_b = b.seal_material(object).await.unwrap(); + assert_eq!(material_b.key.identifier(), material.key.identifier()); + + // ── A revokes B: fresh epoch; B is out, A still seals under epoch 2 ── + let b_id = b.status().device_id.clone().unwrap(); + a.revoke(&b_id).await.unwrap(); + let status = a.status(); + assert_eq!(status.epoch, Some(2)); + let status = b.refresh().await.unwrap(); + assert_eq!(status.phase, VaultPhase::Revoked, "{status:?}"); + let material2 = a.seal_material(object).await.unwrap(); + assert_eq!(material2.binding.epoch, 2); + assert_ne!( + material2.key.identifier(), + material.key.identifier(), + "new epoch, new object key" + ); + let sealed2 = content::seal( + &material2.binding, + ContentPurpose::ChatUpdate, + &material2.key, + &material2.signer, + b"after revocation", + 1024, + ) + .unwrap(); + let untrusted2 = *UnverifiedRecord::parse(sealed2.encoded(), 2048) + .unwrap() + .untrusted_binding(); + // B (revoked) cannot obtain epoch-2 material; its historical epoch-1 + // material still opens the earlier record (accepted history). + assert!(b.open_material(object, &untrusted2).await.is_err()); + assert!(b.open_material(object, &untrusted).await.is_ok()); + + // ── C recovers with the kit (no existing device involved) ─────────── + let c = device(dir_c.path(), &edge, &org, &user); + assert!( + c.recover( + "AAAAA-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA-AAAAA", + None + ) + .await + .is_err() + ); + let genesis = kit.recovery_file["genesisHash"].as_str().map(|h| { + zeron_engine::vault::store::Hex(h.to_string()) + .decode::<32>() + .unwrap() + }); + c.recover(&kit.kit, genesis).await.unwrap(); + assert!(c.is_ready(), "{:?}", c.status().phase); + assert_eq!(c.status().epoch, Some(3), "recovery is a fresh epoch"); + // C holds history: opens A's epoch-1 and epoch-2 records. + let context = c.open_material(object, &untrusted2).await.unwrap(); + let opened = content::open( + sealed2.encoded(), + &context.binding, + ContentPurpose::ChatUpdate, + &context.key, + &context.author_public_key, + 1024, + ) + .unwrap(); + assert_eq!(opened.plaintext().as_bytes(), b"after revocation"); + assert!(c.open_material(object, &untrusted).await.is_ok()); + // A catches up to epoch 3 through the recovery envelope C published. + let status = a.refresh().await.unwrap(); + assert_eq!(status.phase, VaultPhase::Ready, "{status:?}"); + assert_eq!(status.epoch, Some(3)); + let material3 = a.seal_material(object).await.unwrap(); + assert_eq!(material3.binding.epoch, 3); + + // ── Persistence: reopening C's store restores trust without the network + let c_again = device(dir_c.path(), &edge, &org, &user); + let _ = c_again; // fresh MemoryProtection cannot open the file → Locked, never plaintext + let locked = VaultService::open( + VaultStore::new( + dir_c.path(), + format!("{org}/{user}"), + Box::new(MemoryProtection::new()), + ), + None, + &org, + &user, + ); + assert!(matches!( + locked.status().phase, + VaultPhase::Unavailable { .. } | VaultPhase::Locked { .. } + )); + drop(Arc::new(())); +} + +/// The authenticated device channel (RFC 0001 §10) through a REAL DeviceRoom +/// DO: two paired members speak RPC over Noise XX; the relay refuses a +/// plaintext frame for the encrypted profile; a stranger (its own vault, +/// same user) cannot complete the handshake; a revoked member is cut off. +#[tokio::test] +async fn device_channel_over_the_live_relay() { + use zeron_rpc::{ + ChannelHost, HostRelay, HostRelayConfig, LinkCache, LinkCacheConfig, RpcError, RpcReply, + RpcService, StaticToken, methods, + }; + + struct Probe; + #[async_trait::async_trait] + impl RpcService for Probe { + async fn handle( + &self, + method: &str, + params: serde_json::Value, + ) -> Result { + match method { + methods::LIST_HARNESSES => Ok(RpcReply::Value(serde_json::json!([]))), + "Echo" => Ok(RpcReply::Value(params)), + other => Err(RpcError::UnknownMethod(other.into())), + } + } + } + + let Some(edge) = edge_url() else { + eprintln!("ZERON_VAULT_EDGE_URL unset; skipping live vault e2e"); + return; + }; + let (org, user) = fresh_profile(); + let bearer = format!("{user}@{org}"); + let dir_a = tempfile::tempdir().unwrap(); + let dir_b = tempfile::tempdir().unwrap(); + let a = device(dir_a.path(), &edge, &org, &user); + a.refresh().await.unwrap(); + let _kit = a.setup().await.unwrap(); + a.confirm_recovery_kit().await.unwrap(); + let b = device(dir_b.path(), &edge, &org, &user); + b.refresh().await.unwrap(); + let (request_id, code) = b.request_enrollment().await.unwrap(); + a.approve(&request_id, &code).await.unwrap(); + b.refresh().await.unwrap(); + assert!(b.is_ready()); + // A learns B is a member (its head advanced on approval). + a.refresh().await.unwrap(); + + // A hosts its device room with the vault as channel authority. + let relay_device = format!("chan-live-{}", uuid::Uuid::new_v4().simple()); + let mut host_config = HostRelayConfig::new( + edge.clone(), + relay_device.clone(), + Arc::new(StaticToken(bearer.clone())), + ); + host_config.retry = std::time::Duration::from_millis(500); + host_config.channel = Some(ChannelHost { + authority: Arc::new(a.clone()), + service: Arc::new(Probe), + }); + let _host = HostRelay::spawn(host_config, Arc::new(Probe), Arc::new(|_| {})); + + // B dials through the channel and gets an answer. + let mut link_config = LinkCacheConfig::new(edge.clone(), Arc::new(StaticToken(bearer.clone()))); + link_config.probe_timeout = std::time::Duration::from_secs(5); + link_config.cooldown_base = std::time::Duration::from_millis(200); + link_config.cooldown_max = std::time::Duration::from_millis(200); + link_config.channel = Some(Arc::new(b.clone())); + let links = LinkCache::new(link_config); + let client = loop { + match links.client(&relay_device).await { + Ok(client) => break client, + Err(err) => { + eprintln!("dial retry: {err}"); + tokio::time::sleep(std::time::Duration::from_millis(400)).await; + } + } + }; + let echoed = client + .call( + "Echo", + serde_json::json!({ "private": "canary over the relay" }), + ) + .await + .unwrap(); + assert_eq!(echoed["private"], "canary over the relay"); + + // A plaintext client for the same profile: the relay closes the socket + // (4403) before the host ever sees the frame — the dial fails. + let mut plain_config = + LinkCacheConfig::new(edge.clone(), Arc::new(StaticToken(bearer.clone()))); + plain_config.probe_timeout = std::time::Duration::from_secs(5); + let plain = LinkCache::new(plain_config); + let err = match plain.client(&relay_device).await { + Ok(_) => panic!("plaintext relay client must be refused"), + Err(err) => err.to_string(), + }; + eprintln!("plaintext dial refused: {err}"); + + // A stranger: same user at the relay, but its own vault (another org) + // — the handshake prologue and membership both refuse it. + let dir_s = tempfile::tempdir().unwrap(); + let (other_org, _) = fresh_profile(); + let stranger = device(dir_s.path(), &edge, &other_org, &user); + stranger.refresh().await.unwrap(); + stranger.setup().await.unwrap(); + stranger.confirm_recovery_kit().await.unwrap(); + let mut stranger_config = + LinkCacheConfig::new(edge.clone(), Arc::new(StaticToken(bearer.clone()))); + stranger_config.probe_timeout = std::time::Duration::from_secs(5); + stranger_config.channel = Some(Arc::new(stranger.clone())); + let stranger_links = LinkCache::new(stranger_config); + assert!( + stranger_links.client(&relay_device).await.is_err(), + "a device from another vault must not get a channel" + ); + + // Revocation ends B's session: A's next refresh sees B gone and the + // established channel is cut at the next frame; a redial is refused. + let b_id = b.status().device_id.clone().unwrap(); + a.revoke(&b_id).await.unwrap(); + let err = client + .call("Echo", serde_json::json!({})) + .await + .expect_err("revoked member gets no answer"); + eprintln!("post-revocation call: {err}"); + links.invalidate(&relay_device); + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + assert!( + links.client(&relay_device).await.is_err(), + "revoked member must not re-establish the channel" + ); +} + +/// Registry field envelopes (RFC 0001 §9) through the live control plane: +/// one object key per epoch for the whole registry, values bound to their +/// row/field/clock, deletion markers, and key-unavailable withholding. +#[tokio::test] +async fn registry_fields_seal_open_and_bind_their_slot() { + use zeron_engine::vault::VaultRegistryCodec; + use zeron_sync::{FieldOpenFailure, RegistryCodec}; + + let Some(edge) = edge_url() else { + eprintln!("ZERON_VAULT_EDGE_URL unset; skipping live vault e2e"); + return; + }; + let (org, user) = fresh_profile(); + let dir_a = tempfile::tempdir().unwrap(); + let dir_b = tempfile::tempdir().unwrap(); + let a = device(dir_a.path(), &edge, &org, &user); + a.refresh().await.unwrap(); + let _kit = a.setup().await.unwrap(); + a.confirm_recovery_kit().await.unwrap(); + let b = device(dir_b.path(), &edge, &org, &user); + b.refresh().await.unwrap(); + let (request_id, code) = b.request_enrollment().await.unwrap(); + a.approve(&request_id, &code).await.unwrap(); + b.refresh().await.unwrap(); + assert!(b.is_ready()); + + let codec_a = VaultRegistryCodec::new(a.clone(), &user); + let codec_b = VaultRegistryCodec::new(b.clone(), &user); + // Sealing before `prepare` is refused (no half-sealed batches). + assert!( + codec_a + .seal_field( + "chats", + "c1", + "title", + "1-000000-a", + &serde_json::json!("t") + ) + .is_err() + ); + codec_a.prepare().await.unwrap(); + let title = codec_a + .seal_field( + "chats", + "c1", + "title", + "0000000000001-000000-a", + &serde_json::json!("secret title"), + ) + .unwrap(); + let deleted = codec_a + .seal_field( + "chats", + "c1", + "branch", + "0000000000002-000000-a", + &serde_json::Value::Null, + ) + .unwrap(); + let wire = serde_json::to_string(&title).unwrap(); + assert!( + !wire.contains("secret title"), + "plaintext on the wire: {wire}" + ); + assert!(title.get("e1").is_some()); + + // B opens both after fetching the object key from the control plane. + let first = codec_b.open_field("chats", "c1", "title", "0000000000001-000000-a", &title); + if first == Err(FieldOpenFailure::KeyUnavailable) { + // The synchronous path spawned a key fetch; wait for it. + for _ in 0..50 { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + if codec_b + .open_field("chats", "c1", "title", "0000000000001-000000-a", &title) + .is_ok() + { + break; + } + } + } + assert_eq!( + codec_b.open_field("chats", "c1", "title", "0000000000001-000000-a", &title), + Ok(Some(serde_json::json!("secret title"))) + ); + assert_eq!( + codec_b.open_field("chats", "c1", "branch", "0000000000002-000000-a", &deleted), + Ok(None), + "an authenticated deletion marker opens as absence" + ); + // Moved between fields, rows, or clocks: rejected, never displayed. + assert_eq!( + codec_b.open_field("chats", "c1", "cwd", "0000000000001-000000-a", &title), + Err(FieldOpenFailure::Rejected) + ); + assert_eq!( + codec_b.open_field("chats", "c2", "title", "0000000000001-000000-a", &title), + Err(FieldOpenFailure::Rejected) + ); + assert_eq!( + codec_b.open_field("chats", "c1", "title", "0000000000009-000000-a", &title), + Err(FieldOpenFailure::Rejected) + ); + // Plaintext where ciphertext is required is rejected too. + assert_eq!( + codec_b.open_field( + "chats", + "c1", + "title", + "0000000000001-000000-a", + &serde_json::json!("plain") + ), + Err(FieldOpenFailure::Rejected) + ); +} + +/// Opt-in companion for iOS MobileVaultLiveTests. Approval is automatic ONLY +/// inside this fresh, disposable test profile; never run with a real account. +#[tokio::test] +async fn mobile_test_host() { + use zeron_rpc::{ + ChannelHost, HostRelay, HostRelayConfig, RpcError, RpcReply, RpcService, StaticToken, + }; + let Ok(directory) = std::env::var("ZERON_MOBILE_E2E_DIR") else { + return; + }; + let edge = edge_url().expect("ZERON_VAULT_EDGE_URL is required"); + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir_all(&directory).unwrap(); + let (org, user) = fresh_profile(); + let dir = tempfile::tempdir().unwrap(); + let host = device(dir.path(), &edge, &org, &user); + host.refresh().await.unwrap(); + host.setup().await.unwrap(); + host.confirm_recovery_kit().await.unwrap(); + struct Echo; + #[async_trait::async_trait] + impl RpcService for Echo { + async fn handle( + &self, + method: &str, + params: serde_json::Value, + ) -> Result { + match method { + "Echo" => Ok(RpcReply::Value(params)), + "EchoStream" => Ok(RpcReply::Stream(Box::pin(futures::stream::iter( + (0..3).map(|i| serde_json::json!({ "text": format!("stream {i}") })), + )))), + _ => Err(RpcError::UnknownMethod(method.into())), + } + } + } + let relay_id = format!("ios-live-{}", uuid::Uuid::new_v4().simple()); + let mut config = HostRelayConfig::new( + edge.clone(), + relay_id.clone(), + Arc::new(StaticToken(format!("{user}@{org}"))), + ); + config.channel = Some(ChannelHost { + authority: Arc::new(host.clone()), + service: Arc::new(Echo), + }); + let _relay = HostRelay::spawn(config, Arc::new(Echo), Arc::new(|_| {})); + let chat = &format!("mobile-sidecar-{}", uuid::Uuid::new_v4().simple()); + let material = host + .seal_material(object_id_for("chat", chat)) + .await + .unwrap(); + let tail = serde_json::json!({"chatId":chat,"schemaVersion":1,"totalMessages":1,"updatedAt":1000, + "messages":[{"id":"message-1","role":"assistant","createdAt":1000,"deviceId":"host","parts":[ + {"id":"text-1","kind":"text","text":"Encrypted recent messages"}, + {"id":"tool-1","kind":"tool","call":{"kind":"exec","command":"echo hello"},"isError":false, + "resolved":true,"output":"hello","outputRef":format!("{chat}/tool-1")}]}]}); + for (path, purpose, plaintext) in [ + ( + format!("chat2/{chat}-e1/tail"), + ContentPurpose::Tail, + serde_json::to_vec(&tail).unwrap(), + ), + ( + format!("blob/{chat}/tool-1"), + ContentPurpose::Blob, + b"Full encrypted tool output".to_vec(), + ), + ] { + let sealed = content::seal( + &material.binding, + purpose, + &material.key, + &material.signer, + &plaintext, + 4 * 1024 * 1024 - 1024, + ) + .unwrap(); + let response = reqwest::Client::new() + .put(format!("{edge}/{path}")) + .bearer_auth(format!("{user}@{org}")) + .header("content-type", "application/octet-stream") + .body(sealed.encoded().to_vec()) + .send() + .await + .unwrap(); + assert!( + response.status().is_success(), + "sidecar {path}: {}", + response.status() + ); + } + let connection = serde_json::json!({"edge":edge,"org":org,"user":user,"relay":relay_id, + "fingerprint":host.status().genesis_hash,"chat":chat}); + std::fs::write( + directory.join("connection.json"), + serde_json::to_vec(&connection).unwrap(), + ) + .unwrap(); + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(600); + let mut approved = None; + while tokio::time::Instant::now() < deadline { + if directory.join("done").exists() { + return; + } + for request in host.pending_requests().await.unwrap() { + host.approve(&request.request_id, &request.pairing_code) + .await + .unwrap(); + approved = Some(request.device_id); + } + if directory.join("revoke").exists() + && let Some(id) = approved.take() + { + host.revoke(&id).await.unwrap(); + std::fs::write(directory.join("revoked"), b"ok").unwrap(); + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + panic!("iOS test did not finish before deadline"); +} + +#[tokio::test] +async fn running_workspace_switches_to_encrypted_registry_after_setup() { + use zeron_engine::workspace_host::{WorkspaceHost, WorkspaceHostConfig}; + let Some(edge) = edge_url() else { return }; + let (org, user) = fresh_profile(); + let dir = tempfile::tempdir().unwrap(); + let vault = device(dir.path(), &edge, &org, &user); + vault.refresh().await.unwrap(); + let workspace = WorkspaceHost::open( + Arc::new(zeron_sync::DocsStore::open(&dir.path().join("docs")).unwrap()), + WorkspaceHostConfig { + device_id: "laptop".into(), + device_name: "Work laptop".into(), + platform: "macos".into(), + org_id: org.clone(), + user_id: user.clone(), + vault: Some(vault.clone()), + edge: Some(EdgeConfig::with_static_token( + &edge, + format!("{user}@{org}"), + )), + }, + ) + .unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(10), async { + while !workspace.connected() { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + }) + .await + .unwrap(); + vault.setup().await.unwrap(); + vault.confirm_recovery_kit().await.unwrap(); + let client = reqwest::Client::new(); + tokio::time::timeout(std::time::Duration::from_secs(15), async { + loop { + let response = client + .get(format!("{edge}/registry/{org}/e1/rows")) + .bearer_auth(format!("{user}@{org}")) + .send() + .await + .unwrap(); + let body: serde_json::Value = response.json().await.unwrap(); + if workspace.connected() && body.to_string().contains("vaultDeviceId") { + assert!(!body.to_string().contains("Work laptop")); + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + }) + .await + .unwrap(); + assert_eq!( + workspace.read_devices().unwrap()[0].vault_device_id, + vault.status().device_id + ); + workspace.shutdown(); +} + +#[tokio::test] +async fn plaintext_history_migrates_resumably_with_sidecars_and_lineage() { + use base64::Engine as _; + use zeron_engine::doc_host::{DocHost, DocHostConfig}; + use zeron_engine::workspace_host::{WorkspaceHost, WorkspaceHostConfig}; + let Some(edge) = edge_url() else { return }; + let (org, user) = fresh_profile(); + let dir = tempfile::tempdir().unwrap(); + let vault = device(dir.path(), &edge, &org, &user); + let config = EdgeConfig::with_static_token(&edge, format!("{user}@{org}")); + let store = Arc::new(zeron_sync::DocsStore::open(dir.path().join("docs")).unwrap()); + let workspace = WorkspaceHost::open( + store.clone(), + WorkspaceHostConfig { + device_id: "laptop".into(), + device_name: "Laptop".into(), + platform: "macos".into(), + org_id: org.clone(), + user_id: user.clone(), + vault: Some(vault.clone()), + edge: Some(config.clone()), + }, + ) + .unwrap(); + let chat = format!("migrate-{}", uuid::Uuid::new_v4().simple()); + workspace + .create_chat(&chat, None, Some("laptop"), None, None) + .unwrap(); + let source = zeron_doc::SessionDoc::init(&chat).unwrap(); + let message = |id: &str, text: &str| { + serde_json::from_value::(serde_json::json!({ + "id":id,"role":"user","parts":[{"id":format!("{id}-text"),"kind":"text","text":text}], + "createdAt":1234,"deviceId":"laptop" + })) + .unwrap() + }; + source + .push_message(&message("before", "Existing private history")) + .unwrap(); + // A real >1 MiB checkpoint must not be shoved into the relay's row path. + use sha2::{Digest, Sha256}; + let mut large = String::new(); + for n in 0..50_000u64 { + for byte in Sha256::digest(n.to_le_bytes()) { + large.push_str(&format!("{byte:02x}")); + } + } + source + .doc() + .get_map("migrationFixture") + .insert("largeHistory", large) + .unwrap(); + source.doc().commit(); + store + .save_snapshot_with_cursor(&chat, &source.export_snapshot().unwrap(), 0, 2) + .unwrap(); + store.mark_processed("already-run").unwrap(); + source + .push_message(&message("remote", "Only present on the relay")) + .unwrap(); + source + .doc() + .get_map("migrationFixture") + .insert("outputRef", format!("{chat}/tool-output")) + .unwrap(); + source.doc().commit(); + let plaintext_checkpoint = source.export_snapshot().unwrap(); + assert!(plaintext_checkpoint.len() > 1024 * 1024); + let frontier = source.doc().oplog_vv(); + let http = reqwest::Client::new(); + let auth = format!("{user}@{org}"); + let response = http + .post(format!("{edge}/chat2/{chat}/checkpoint?seqCovered=0")) + .bearer_auth(&auth) + .header( + "x-chat2-frontier", + base64::engine::general_purpose::STANDARD.encode(frontier.encode()), + ) + .body(plaintext_checkpoint.clone()) + .send() + .await + .unwrap(); + assert!(response.status().is_success(), "{}", response.status()); + source + .push_message(&message("last-row", "Final plaintext row")) + .unwrap(); + let update = source + .doc() + .export(loro::ExportMode::updates(&frontier)) + .unwrap(); + let response = http + .post(format!( + "{edge}/chat2/{chat}/rows?batchId=source-final-row&device=laptop" + )) + .bearer_auth(&auth) + .body(update) + .send() + .await + .unwrap(); + assert!(response.status().is_success(), "{}", response.status()); + let kit = vault.setup().await.unwrap(); + vault.confirm_recovery_kit().await.unwrap(); + let host = DocHost::new( + store.clone(), + DocHostConfig { + device_id: "laptop".into(), + default_harness: zeron_proto::HarnessId::ClaudeCode, + edge: Some(config.clone()), + }, + ); + host.set_vault(vault.clone()); + host.set_workspace(workspace.clone()); + // Missing referenced output must pause migration, not silently drop it. + host.migrate_history().await.unwrap_err(); + assert_eq!(host.history_migration_status().phase, "paused"); + assert_eq!(host.history_migration_status().completed, 0); + host.shutdown_workers().await; + // A reader already waiting in the encrypted room must reconnect when + // migration seeds a checkpoint without adding any log rows. + use futures::StreamExt; + use tokio_tungstenite::tungstenite::client::IntoClientRequest; + let ws_url = format!("{}/chat2/{chat}-e1/ws", edge.replacen("http", "ws", 1)); + let mut request = ws_url.into_client_request().unwrap(); + request + .headers_mut() + .insert("Authorization", format!("Bearer {auth}").parse().unwrap()); + let (mut reader, _) = tokio_tungstenite::connect_async(request).await.unwrap(); + let host = DocHost::new( + store.clone(), + DocHostConfig { + device_id: "laptop".into(), + default_harness: zeron_proto::HarnessId::ClaudeCode, + edge: Some(config.clone()), + }, + ); + host.set_vault(vault.clone()); + host.set_workspace(workspace.clone()); + let codec = zeron_engine::chat2_host::ChatCodec::new(vault.clone(), &chat); + let restored_blob = codec + .seal(ContentPurpose::Blob, b"Recovered full tool output", 1024) + .await + .unwrap(); + assert!( + http.put(format!("{edge}/blob/{chat}/tool-output")) + .bearer_auth(&auth) + .body(restored_blob.encoded().to_vec()) + .send() + .await + .unwrap() + .status() + .is_success() + ); + host.migrate_history().await.unwrap(); + assert_eq!(host.history_migration_status().phase, "complete"); + tokio::time::timeout(std::time::Duration::from_secs(5), async { + while let Some(frame) = reader.next().await { + if let tokio_tungstenite::tungstenite::Message::Close(Some(close)) = frame.unwrap() { + assert_eq!(u16::from(close.code), 4411); + return; + } + } + panic!("migration did not refresh the existing reader"); + }) + .await + .unwrap(); + assert_eq!(host.history_migration_status().completed, 1); + let sealed = http + .get(format!("{edge}/chat2/{chat}-e1/checkpoint")) + .bearer_auth(&auth) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + assert!(!sealed.windows(24).any(|w| w == b"Existing private history")); + let stale_frontier = codec + .seal(ContentPurpose::Frontier, b"stale", 1024) + .await + .unwrap(); + let conflict = http + .post(format!( + "{edge}/chat2/{chat}-e1/checkpoint?seqCovered=0&refreshReaders=1" + )) + .bearer_auth(&auth) + .header( + "x-chat2-frontier", + base64::engine::general_purpose::STANDARD.encode(stale_frontier.encoded()), + ) + .header("x-chat2-expected-frontier", "") + .body(sealed.to_vec()) + .send() + .await + .unwrap(); + assert_eq!(conflict.status(), reqwest::StatusCode::CONFLICT); + let opened = codec + .open_async( + ContentPurpose::Checkpoint, + &sealed, + content::MAX_PLAINTEXT_BYTES, + ) + .await + .unwrap(); + let restored = loro::LoroDoc::new(); + restored.import(&opened).unwrap(); + assert!(restored.oplog_vv().includes_vv(&source.doc().oplog_vv())); + let restored = zeron_doc::SessionDoc::from_doc(restored); + assert_eq!(restored.read_entries().unwrap().len(), 3); + assert!(store.is_processed("already-run").unwrap()); + assert_eq!( + host.fetch_tool_blob(&format!("{chat}/tool-output")) + .await + .unwrap(), + "Recovered full tool output" + ); + let retained = http + .get(format!("{edge}/chat2/{chat}/checkpoint")) + .bearer_auth(&auth) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + assert_eq!(retained.as_ref(), plaintext_checkpoint.as_slice()); + host.migrate_history().await.unwrap(); + assert_eq!( + host.open(&chat) + .unwrap() + .doc() + .read_entries() + .unwrap() + .len(), + 3 + ); + let recovered_dir = tempfile::tempdir().unwrap(); + let recovered = device(recovered_dir.path(), &edge, &org, &user); + let genesis = + zeron_engine::vault::store::Hex(kit.recovery_file["genesisHash"].as_str().unwrap().into()) + .decode::<32>() + .unwrap(); + recovered.recover(&kit.kit, Some(genesis)).await.unwrap(); + let recovery_codec = zeron_engine::chat2_host::ChatCodec::new(recovered, &chat); + let recovered_bytes = recovery_codec + .open_async( + ContentPurpose::Checkpoint, + &sealed, + content::MAX_PLAINTEXT_BYTES, + ) + .await + .unwrap(); + assert_eq!(recovered_bytes, opened); + host.shutdown_workers().await; +} diff --git a/crates/engine/tests/workspace_sync.rs b/crates/engine/tests/workspace_sync.rs index 08ab3fe20..c3ccf98ca 100644 --- a/crates/engine/tests/workspace_sync.rs +++ b/crates/engine/tests/workspace_sync.rs @@ -626,6 +626,7 @@ async fn legacy_workspace_doc_migrates_instantly_on_first_boot() { let now = chrono::Utc::now(); legacy .upsert_device(&Device { + vault_device_id: None, id: "dev-a".into(), name: "old laptop".into(), platform: "linux".into(), diff --git a/crates/proto/src/entities.rs b/crates/proto/src/entities.rs index 2c252d59e..e1a282c36 100644 --- a/crates/proto/src/entities.rs +++ b/crates/proto/src/entities.rs @@ -14,6 +14,9 @@ pub struct Device { pub id: String, pub name: String, pub platform: String, + /// Vault identity associated with this device row (display metadata only). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vault_device_id: Option, pub last_seen_at: Option>, /// First registration time (zeron devices.created_at — the Devices page /// "Added …" fragment). Optional so pre-existing docs stay readable. diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index b5a4ca893..f877560f9 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -7,6 +7,7 @@ publish.workspace = true [dependencies] zeron-proto.workspace = true +zeron-crypto.workspace = true zeron-sync.workspace = true tokio.workspace = true tokio-tungstenite.workspace = true diff --git a/crates/rpc/src/device_channel.rs b/crates/rpc/src/device_channel.rs new file mode 100644 index 000000000..1325fb6ed --- /dev/null +++ b/crates/rpc/src/device_channel.rs @@ -0,0 +1,126 @@ +//! The authenticated device channel over the relay (RFC 0001 §10, plan ES-11). +//! +//! The relay DO only routes bytes. For an enrolled profile every RPC byte +//! that crosses it is wrapped by a Noise XX session between the two devices' +//! vault identities ([`zeron_crypto::channel`]); the relay, the edge, and +//! anyone holding the edge's storage see handshake and ciphertext only. +//! +//! Wire shape, on top of the ordinary `{s, k, to?, from?}` frame header: +//! - kind [`CHANNEL_KIND`] for every channel frame; +//! - stream id [`CHANNEL_HS1`] / [`CHANNEL_HS2`] / [`CHANNEL_HS3`] carries the +//! three Noise handshake messages (client → host, host → client, client → +//! host); +//! - stream id [`CHANNEL_DATA`] carries one sealed RPC line per frame; +//! - stream id [`CHANNEL_ERROR`] (host → client) carries `{"error": code}` and +//! ends the client's link: [`CHANNEL_REQUIRED`] when a plaintext frame +//! reached an enrolled host, [`CHANNEL_REJECTED`] when the handshake or a +//! sealed frame failed verification or the peer is not an active member. +//! +//! Who decides membership is the [`ChannelAuthority`]: the engine implements +//! it over its vault (device id, X25519 static, vault + generation scope, +//! and the active-member lookup). The rpc crate never sees key material +//! beyond the [`ChannelIdentity`] it is handed for one handshake. + +use std::sync::{Arc, Mutex}; + +use serde::Deserialize; +use zeron_crypto::channel::{Channel, ChannelIdentity, ChannelScope, PeerIdentity}; + +use crate::{RpcError, RpcService}; + +pub const CHANNEL_KIND: &str = "chan"; +pub const CHANNEL_HS1: &str = "hs1"; +pub const CHANNEL_HS2: &str = "hs2"; +pub const CHANNEL_HS3: &str = "hs3"; +pub const CHANNEL_DATA: &str = "rpc"; +pub const CHANNEL_ERROR: &str = "err"; + +/// Error codes on [`CHANNEL_ERROR`] frames. +pub const CHANNEL_REQUIRED: &str = "encrypted_channel_required"; +pub const CHANNEL_REJECTED: &str = "channel_rejected"; +pub const CHANNEL_UNSUPPORTED: &str = "channel_unsupported"; + +/// This device's side of a channel: identity plus the vault scope both +/// peers must share (prologue material). +pub struct ChannelLocal { + pub identity: ChannelIdentity, + pub scope: ChannelScope, +} + +/// Membership authority for the device channel — implemented by the engine +/// over its vault. Every method is called synchronously and must not block. +pub trait ChannelAuthority: Send + Sync + 'static { + /// True once this profile is enrolled in a vault: from then on the host + /// refuses plaintext relay frames and the client dials only through the + /// channel. Never falls back. + fn required(&self) -> bool; + + /// This device's channel identity and scope, or a human-readable reason + /// the channel cannot be established right now (locked store, not an + /// active member, verification failure). + fn local(&self) -> Result; + + /// The membership check on a handshake peer: true only for an ACTIVE + /// member of the same vault whose published encryption key is + /// `peer.static_key` and whose device id is `peer.device_id`, and which + /// is not this device itself. Re-checked on every inbound sealed frame + /// so a revocation that lands locally ends the session. + fn accept(&self, peer: &PeerIdentity) -> bool; +} + +/// The host relay's channel configuration: the authority and the service +/// served to channel-authenticated peers (the plaintext service stays +/// gated for enrolled profiles). +pub struct ChannelHost { + pub authority: Arc, + pub service: Arc, +} + +/// An established channel shared between the inbound loop and the outbound +/// pump. Sealing and opening are short synchronous operations. +pub(crate) type SharedChannel = Arc>; + +pub(crate) fn shared(channel: Channel) -> SharedChannel { + Arc::new(Mutex::new(channel)) +} + +pub(crate) fn seal(channel: &SharedChannel, text: &str) -> Result, RpcError> { + channel + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .seal(text.as_bytes()) + .map_err(|e| RpcError::Transport(format!("device channel seal: {e}"))) +} + +pub(crate) fn open(channel: &SharedChannel, sealed: &[u8]) -> Result { + let bytes = channel + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .open(sealed) + .map_err(|e| RpcError::Transport(format!("device channel open: {e}")))?; + String::from_utf8(bytes) + .map_err(|_| RpcError::Transport("device channel: non-UTF-8 RPC frame".into())) +} + +pub(crate) fn peer_of(channel: &SharedChannel) -> PeerIdentity { + *channel + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .peer() +} + +pub(crate) fn error_payload(code: &str) -> Vec { + serde_json::json!({ "error": code }) + .to_string() + .into_bytes() +} + +pub(crate) fn error_code(payload: &[u8]) -> String { + #[derive(Deserialize)] + struct Code { + error: String, + } + serde_json::from_slice::(payload) + .map(|c| c.error) + .unwrap_or_else(|_| "channel error".into()) +} diff --git a/crates/rpc/src/device_room.rs b/crates/rpc/src/device_room.rs index f7658f89e..d39bb94e5 100644 --- a/crates/rpc/src/device_room.rs +++ b/crates/rpc/src/device_room.rs @@ -26,6 +26,13 @@ use serde::{Deserialize, Serialize}; use tokio::sync::{mpsc, watch}; use tokio_tungstenite::tungstenite::Message as WsMessage; +use zeron_crypto::channel::{Handshake, PeerIdentity}; + +use crate::device_channel::{ + CHANNEL_DATA, CHANNEL_ERROR, CHANNEL_HS1, CHANNEL_HS2, CHANNEL_HS3, CHANNEL_KIND, + CHANNEL_REJECTED, CHANNEL_REQUIRED, CHANNEL_UNSUPPORTED, ChannelAuthority, ChannelHost, + SharedChannel, error_code, error_payload, open, peer_of, seal, shared, +}; use crate::{RpcClient, RpcError, RpcService, serve_connection}; /// Relay-emitted control frames. MUST byte-match the DO's `RELAY_KIND` (yes, it has a @@ -270,6 +277,10 @@ pub struct HostRelayConfig { pub token: Arc, /// Reconnect delay after a session ends (a small jitter is added). pub retry: Duration, + /// The authenticated device channel (RFC 0001 §10). `None` = this build + /// cannot host one (no vault); an enrolled profile then refuses every + /// relay client rather than serving plaintext. + pub channel: Option, } impl HostRelayConfig { @@ -283,6 +294,7 @@ impl HostRelayConfig { device_id: device_id.into(), token, retry: Duration::from_secs(5), + channel: None, } } } @@ -324,7 +336,8 @@ impl HostRelay { ); let started = tokio::time::Instant::now(); let outcome = { - let session = host_session(&url, &service, &on_nudge); + let session = + host_session(&url, &service, &on_nudge, config.channel.as_ref()); tokio::pin!(session); loop { tokio::select! { @@ -417,18 +430,54 @@ struct VirtualConn { in_tx: mpsc::Sender, } +/// Per-client host state. A conn id is plaintext OR channel — never both: +/// a handshake on an id replaces whatever it was, and plaintext frames on a +/// secured id are refused. +enum HostConn { + Plain(VirtualConn), + Handshaking(Handshake), + Secured { + conn: VirtualConn, + channel: SharedChannel, + }, +} + +/// `channel` = seal every reply through the device channel (frames go out as +/// [`CHANNEL_DATA`]); `None` = plaintext [`RPC_KIND`] frames. fn make_virtual_conn( service: Arc, conn_id: String, host_out: mpsc::Sender>, + channel: Option, ) -> VirtualConn { let (in_tx, in_rx) = mpsc::channel::(256); let (srv_out_tx, mut srv_out_rx) = mpsc::channel::(256); tokio::spawn(serve_connection(service, srv_out_tx, in_rx)); tokio::spawn(async move { while let Some(text) = srv_out_rx.recv().await { - let header = DeviceFrameHeader::new(RPC_KIND, RPC_KIND).with_to(conn_id.clone()); - match encode_device_frame(&header, text.as_bytes()) { + let (header, payload) = match &channel { + Some(channel) => match seal(channel, &text) { + Ok(sealed) => (DeviceFrameHeader::new(CHANNEL_DATA, CHANNEL_KIND), sealed), + Err(err) => { + // Budget spent or a poisoned session: end the link + // rather than ever sending the reply another way. + tracing::warn!(error = %err, "device-room: channel seal failed; closing conn"); + let header = + DeviceFrameHeader::new(CHANNEL_ERROR, CHANNEL_KIND).with_to(conn_id); + if let Ok(frame) = + encode_device_frame(&header, &error_payload(CHANNEL_REJECTED)) + { + let _ = host_out.send(frame).await; + } + break; + } + }, + None => ( + DeviceFrameHeader::new(RPC_KIND, RPC_KIND), + text.into_bytes(), + ), + }; + match encode_device_frame(&header.with_to(conn_id.clone()), &payload) { Ok(frame) => { if host_out.send(frame).await.is_err() { break; // relay socket gone @@ -446,6 +495,7 @@ async fn host_session( url: &str, service: &Arc, on_nudge: &NudgeHandler, + channel: Option<&ChannelHost>, ) -> Result<(), RpcError> { let ws = zeron_sync::dial::connect_ws(url) .await @@ -454,7 +504,7 @@ async fn host_session( let (mut sink, mut stream) = ws.split(); // All writers (per-conn pumps) funnel through one outbound queue → one socket writer. let (out_tx, mut out_rx) = mpsc::channel::>(256); - let mut conns: HashMap = HashMap::new(); + let mut conns: HashMap = HashMap::new(); let mut ping = tokio::time::interval(PING_INTERVAL); ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); ping.tick().await; // consume the immediate first tick @@ -473,7 +523,8 @@ async fn host_session( message = stream.next() => match message { Some(Ok(WsMessage::Binary(bytes))) => { last_rx = tokio::time::Instant::now(); - handle_host_frame(&bytes, &mut conns, service, &out_tx, on_nudge).await; + handle_host_frame(&bytes, &mut conns, service, &out_tx, on_nudge, channel) + .await; } Some(Ok(WsMessage::Close(frame))) => { if let Some(frame) = frame { @@ -505,12 +556,122 @@ async fn host_session( Ok(()) } +async fn send_channel_error(out_tx: &mpsc::Sender>, to: &str, code: &str) { + let header = DeviceFrameHeader::new(CHANNEL_ERROR, CHANNEL_KIND).with_to(to); + match encode_device_frame(&header, &error_payload(code)) { + Ok(frame) => { + let _ = out_tx.send(frame).await; + } + Err(err) => tracing::error!(error = %err, "device-room: channel error encode failed"), + } +} + +/// The channel state machine for one client conn (frames of kind +/// [`CHANNEL_KIND`]). Any failure drops the conn's state and tells the +/// client; nothing about the failure is ever answered in plaintext. +async fn handle_channel_frame( + header: &DeviceFrameHeader, + payload: &[u8], + from: &str, + conns: &mut HashMap, + out_tx: &mpsc::Sender>, + host: &ChannelHost, +) { + match header.s.as_str() { + CHANNEL_HS1 => { + // A fresh handshake on a known id replaces its state (a client + // that reconnected under the same conn id after a drop). + conns.remove(from); + let started = host.authority.local().map_err(|reason| { + RpcError::Transport(format!("device channel unavailable: {reason}")) + }); + let started = started.and_then(|local| { + Handshake::respond(&local.identity, &local.scope, payload) + .map_err(|e| RpcError::Transport(format!("device channel handshake: {e}"))) + }); + match started { + Ok((handshake, second)) => { + conns.insert(from.to_string(), HostConn::Handshaking(handshake)); + let reply = DeviceFrameHeader::new(CHANNEL_HS2, CHANNEL_KIND).with_to(from); + match encode_device_frame(&reply, &second) { + Ok(frame) => { + let _ = out_tx.send(frame).await; + } + Err(err) => { + tracing::error!(error = %err, "device-room: handshake encode failed") + } + } + } + Err(err) => { + tracing::warn!(conn = %from, error = %err, "device-room: channel handshake refused"); + send_channel_error(out_tx, from, CHANNEL_REJECTED).await; + } + } + } + CHANNEL_HS3 => { + let Some(HostConn::Handshaking(mut handshake)) = conns.remove(from) else { + send_channel_error(out_tx, from, CHANNEL_REJECTED).await; + return; + }; + let finished = handshake + .responder_step(payload) + .and_then(|_| handshake.finish(|peer| host.authority.accept(peer))); + match finished { + Ok(channel) => { + let channel = shared(channel); + let conn = make_virtual_conn( + host.service.clone(), + from.to_string(), + out_tx.clone(), + Some(channel.clone()), + ); + tracing::info!(conn = %from, "device-room: device channel established"); + conns.insert(from.to_string(), HostConn::Secured { conn, channel }); + } + Err(err) => { + tracing::warn!(conn = %from, error = %err, "device-room: channel peer refused"); + send_channel_error(out_tx, from, CHANNEL_REJECTED).await; + } + } + } + CHANNEL_DATA => { + let Some(HostConn::Secured { conn, channel }) = conns.get(from) else { + send_channel_error(out_tx, from, CHANNEL_REJECTED).await; + return; + }; + // Membership is re-checked per frame: a revocation that has + // reached this device ends the session at the next byte. + let opened = if host.authority.accept(&peer_of(channel)) { + open(channel, payload) + } else { + Err(RpcError::Transport( + "peer is no longer an active member".into(), + )) + }; + match opened { + Ok(text) => { + if conn.in_tx.send(text).await.is_err() { + tracing::warn!("device-room: virtual conn dispatch loop gone"); + } + } + Err(err) => { + tracing::warn!(conn = %from, error = %err, "device-room: sealed frame refused"); + conns.remove(from); + send_channel_error(out_tx, from, CHANNEL_REJECTED).await; + } + } + } + _ => {} + } +} + async fn handle_host_frame( bytes: &[u8], - conns: &mut HashMap, + conns: &mut HashMap, service: &Arc, out_tx: &mpsc::Sender>, on_nudge: &NudgeHandler, + channel: Option<&ChannelHost>, ) { let (header, payload) = match decode_device_frame(bytes) { Ok(frame) => frame, @@ -559,15 +720,41 @@ async fn handle_host_frame( } return; } + if header.k == CHANNEL_KIND { + let Some(from) = header.from.clone() else { + return; + }; + match channel { + Some(host) => handle_channel_frame(&header, &payload, &from, conns, out_tx, host).await, + None => send_channel_error(out_tx, &from, CHANNEL_UNSUPPORTED).await, + } + return; + } if header.k != RPC_KIND { return; // future stream kinds (term, tunnel) } let Some(from) = header.from else { return; }; - let conn = conns - .entry(from.clone()) - .or_insert_with(|| make_virtual_conn(service.clone(), from, out_tx.clone())); + // Enrolled profile: plaintext RPC is refused outright — the client is + // told why and its link ends. There is no downgrade path. + if channel.is_some_and(|host| host.authority.required()) { + tracing::warn!(conn = %from, "device-room: plaintext RPC refused (encrypted channel required)"); + conns.remove(&from); + send_channel_error(out_tx, &from, CHANNEL_REQUIRED).await; + return; + } + let entry = conns.entry(from.clone()).or_insert_with(|| { + HostConn::Plain(make_virtual_conn( + service.clone(), + from, + out_tx.clone(), + None, + )) + }); + let HostConn::Plain(conn) = entry else { + return; // plaintext on a channel conn id: dropped + }; let text = String::from_utf8_lossy(&payload).into_owned(); if conn.in_tx.send(text).await.is_err() { tracing::warn!("device-room: virtual conn dispatch loop gone"); @@ -586,14 +773,112 @@ pub struct DeviceLink { client: Arc, closed_rx: watch::Receiver>, pump: tokio::task::JoinHandle<()>, + peer: Option, +} + +type WsSink = futures::stream::SplitSink; +type WsSource = futures::stream::SplitStream; + +/// Run the initiator side of the Noise XX handshake over a fresh relay +/// socket. The host's identity is checked against membership BEFORE this +/// device sends its own (message 3), so a stranger holding the relay learns +/// nothing but an ephemeral key. +async fn client_handshake( + authority: &dyn ChannelAuthority, + sink: &mut WsSink, + stream: &mut WsSource, +) -> Result { + let transport = |m: String| RpcError::Transport(m); + let local = authority + .local() + .map_err(|reason| transport(format!("device channel unavailable: {reason}")))?; + let (mut handshake, first) = Handshake::initiate(&local.identity, &local.scope) + .map_err(|e| transport(format!("device channel handshake: {e}")))?; + let frame = encode_device_frame(&DeviceFrameHeader::new(CHANNEL_HS1, CHANNEL_KIND), &first)?; + sink.send(WsMessage::Binary(frame)) + .await + .map_err(|_| transport("connection lost during handshake".into()))?; + loop { + match stream.next().await { + Some(Ok(WsMessage::Binary(bytes))) => { + let (header, payload) = decode_device_frame(&bytes)?; + if header.k == RELAY_KIND { + let code = relay_error_code(&payload).unwrap_or_else(|| "relay error".into()); + return Err(transport(code)); + } + if header.k == RPC_KIND { + return Err(transport( + "plaintext RPC frame during device channel handshake".into(), + )); + } + if header.k != CHANNEL_KIND { + continue; // echoes and future kinds + } + if header.s == CHANNEL_ERROR { + return Err(transport(format!( + "device channel refused: {}", + error_code(&payload) + ))); + } + if header.s != CHANNEL_HS2 { + return Err(transport("unexpected device channel frame".into())); + } + let (third, _) = handshake + .initiator_step(&payload) + .map_err(|e| transport(format!("device channel handshake: {e}")))?; + let channel = handshake + .finish(|peer| authority.accept(peer)) + .map_err(|e| transport(format!("device channel peer refused: {e}")))?; + let frame = encode_device_frame( + &DeviceFrameHeader::new(CHANNEL_HS3, CHANNEL_KIND), + &third, + )?; + sink.send(WsMessage::Binary(frame)) + .await + .map_err(|_| transport("connection lost during handshake".into()))?; + return Ok(shared(channel)); + } + Some(Ok(WsMessage::Close(_))) | Some(Err(_)) | None => { + return Err(transport("connection lost during handshake".into())); + } + Some(Ok(_)) => {} + } + } } impl DeviceLink { + /// Plaintext link (profiles with no vault). pub async fn connect(url: &str) -> Result { + Self::connect_inner(url, None).await + } + + /// Link through the authenticated device channel: the handshake completes + /// (and both memberships check out) before any RPC byte is accepted. + pub async fn connect_secured( + url: &str, + authority: &dyn ChannelAuthority, + ) -> Result { + Self::connect_inner(url, Some(authority)).await + } + + /// The channel peer this link authenticated (`None` on plaintext links). + pub fn peer(&self) -> Option { + self.peer + } + + async fn connect_inner( + url: &str, + authority: Option<&dyn ChannelAuthority>, + ) -> Result { let ws = zeron_sync::dial::connect_ws(url) .await .map_err(|e| RpcError::Transport(format!("device room unreachable: {e}")))?; let (mut sink, mut stream) = ws.split(); + let channel = match authority { + Some(authority) => Some(client_handshake(authority, &mut sink, &mut stream).await?), + None => None, + }; + let peer = channel.as_ref().map(peer_of); let (out_tx, mut out_rx) = mpsc::channel::(256); let (in_tx, in_rx) = mpsc::channel::(256); let (closed_tx, closed_rx) = watch::channel::>(None); @@ -620,8 +905,17 @@ impl DeviceLink { tokio::select! { frame = out_rx.recv() => match frame { Some(text) => { - let header = DeviceFrameHeader::new(RPC_KIND, RPC_KIND); - let encoded = match encode_device_frame(&header, text.as_bytes()) { + let (header, payload) = match &channel { + Some(channel) => match seal(channel, &text) { + Ok(sealed) => (DeviceFrameHeader::new(CHANNEL_DATA, CHANNEL_KIND), sealed), + Err(err) => { + tracing::warn!(error = %err, "device-room: channel seal failed; link down"); + break "device channel exhausted".to_string(); + } + }, + None => (DeviceFrameHeader::new(RPC_KIND, RPC_KIND), text.into_bytes()), + }; + let encoded = match encode_device_frame(&header, &payload) { Ok(bytes) => bytes, Err(err) => { tracing::error!(error = %err, "device-room: frame encode failed"); @@ -648,6 +942,36 @@ impl DeviceLink { tracing::info!(%code, "device-room: link down"); break code; } + Ok((header, payload)) if header.k == CHANNEL_KIND => { + if header.s == CHANNEL_ERROR { + let code = error_code(&payload); + tracing::info!(%code, "device-room: link refused by host"); + break code; + } + let Some(channel) = &channel else { + break "unexpected device channel frame".to_string(); + }; + if header.s != CHANNEL_DATA { + break "unexpected device channel frame".to_string(); + } + let opened = match open(channel, &payload) { + Ok(text) => text, + Err(err) => { + tracing::warn!(error = %err, "device-room: sealed frame refused; link down"); + break "device channel verification failed".to_string(); + } + }; + last_echo = tokio::time::Instant::now(); + if in_tx.send(opened).await.is_err() { + break "client dropped".to_string(); + } + } + Ok((header, _)) if header.k == RPC_KIND && channel.is_some() => { + // A secured link never accepts plaintext + // from the host: it is a downgrade attempt + // or a confused host — either way, down. + break "plaintext frame on secured link".to_string(); + } Ok((header, payload)) if header.k == RPC_KIND => { // An RPC frame from the host proves the // whole path just as well as an echo. @@ -700,6 +1024,7 @@ impl DeviceLink { client: Arc::new(RpcClient::new(out_tx, in_rx)), closed_rx, pump, + peer, }) } @@ -756,6 +1081,10 @@ pub struct LinkCacheConfig { /// is presence-driven: the workspace's peer-alive hook fires the moment /// heartbeats return, and the next call passes the gate. pub liveness: Option, + /// The device channel authority. Once it reports `required()`, every + /// dial runs the authenticated handshake first and a host that cannot + /// complete it is unreachable — never dialed in plaintext. + pub channel: Option>, } impl LinkCacheConfig { @@ -773,6 +1102,7 @@ impl LinkCacheConfig { cooldown_max: Duration::from_secs(60), probe_timeout: Duration::from_secs(10), liveness: None, + channel: None, } } } @@ -819,23 +1149,21 @@ impl LinkCache { // Skipped outside a runtime (sync unit tests). if tokio::runtime::Handle::try_current().is_ok() { let weak = Arc::downgrade(&cache); + let mut token_changes = cache.config.token.subscribe(); tokio::spawn(async move { let mut wake = zeron_sync::wake::subscribe(); let mut online = zeron_sync::wake::subscribe_online(); - let mut token_changes = weak - .upgrade() - .and_then(|cache| cache.config.token.subscribe()); loop { tokio::select! { result = wake.recv() => { - if result.is_err() { return; } + if matches!(result, Err(tokio::sync::broadcast::error::RecvError::Closed)) { return; } let Some(cache) = weak.upgrade() else { return }; lock(&cache.links).clear(); lock(&cache.dial_state).clear(); tracing::info!("peer: links + cooldowns cleared after wake"); } result = online.recv() => { - if result.is_err() { return; } + if matches!(result, Err(tokio::sync::broadcast::error::RecvError::Closed)) { return; } let Some(cache) = weak.upgrade() else { return }; lock(&cache.dial_state).clear(); } @@ -1027,7 +1355,18 @@ impl LinkCache { // under `forward()` — a wedged edge socket hung callers indefinitely // and only the UI's own per-call timers saved them (silently). const DIAL_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); - let link = tokio::time::timeout(DIAL_CONNECT_TIMEOUT, DeviceLink::connect(&url)) + let secured = self + .config + .channel + .as_ref() + .filter(|authority| authority.required()); + let connect = async { + match secured { + Some(authority) => DeviceLink::connect_secured(&url, authority.as_ref()).await, + None => DeviceLink::connect(&url).await, + } + }; + let link = tokio::time::timeout(DIAL_CONNECT_TIMEOUT, connect) .await .map_err(|_| RpcError::Transport(format!("peer {device_id}: connect timed out")))?; let link = Arc::new(link?); @@ -1041,7 +1380,14 @@ impl LinkCache { RpcError::Transport(format!("peer {device_id}: readiness check timed out")) })? .map_err(|e| { - RpcError::Transport(format!("peer {device_id}: readiness check failed: {e}")) + // A link the host refused (plaintext on an enrolled profile, + // a rejected channel) closes with a reason worth more to the + // caller than the resulting `Closed` on the probe call. + let reason = link.closed().borrow().clone(); + RpcError::Transport(match reason { + Some(reason) => format!("peer {device_id}: readiness check failed: {reason}"), + None => format!("peer {device_id}: readiness check failed: {e}"), + }) })?; Ok(link) } diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index a2372bf60..da5851b08 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -19,10 +19,12 @@ use futures::stream::BoxStream; use serde::{Deserialize, Serialize}; mod client; +pub mod device_channel; pub mod device_room; mod server; pub use client::{RpcClient, RpcSubscription, connect_ws}; +pub use device_channel::{ChannelAuthority, ChannelHost, ChannelLocal}; pub use device_room::{ DeviceFrameHeader, DeviceLink, HostRelay, HostRelayConfig, LinkCache, LinkCacheConfig, NudgeHandler, PeerLiveness, PeerLivenessProbe, StaticToken, TokenSource, decode_device_frame, @@ -108,6 +110,21 @@ pub mod methods { /// Readiness barrier for the engine runtime. The call completes once stores /// and journals are assembled, or fails with the assembly error. pub const ENGINE_READY: &str = "EngineReady"; + // Encrypted-sync vault (RFC 0001 §4, §6, §11): explicit lifecycle + // operations; none of them exports private keys. + pub const VAULT_STATUS: &str = "VaultStatus"; + pub const VAULT_REFRESH: &str = "VaultRefresh"; + pub const VAULT_MIGRATE_HISTORY: &str = "VaultMigrateHistory"; + pub const VAULT_SETUP: &str = "VaultSetup"; + pub const VAULT_CONFIRM_RECOVERY: &str = "VaultConfirmRecovery"; + pub const VAULT_REQUEST_ENROLLMENT: &str = "VaultRequestEnrollment"; + pub const VAULT_CANCEL_ENROLLMENT: &str = "VaultCancelEnrollment"; + pub const VAULT_PENDING_REQUESTS: &str = "VaultPendingRequests"; + pub const VAULT_APPROVE: &str = "VaultApprove"; + pub const VAULT_REJECT: &str = "VaultReject"; + pub const VAULT_RENAME_DEVICE: &str = "VaultRenameDevice"; + pub const VAULT_REVOKE: &str = "VaultRevoke"; + pub const VAULT_RECOVER: &str = "VaultRecover"; /// Ask a headless IPC owner to drain its runtime and exit successfully. /// Headed IPC owners do not implement this method: closing another app's /// engine behind its windows would leave that process unusable. diff --git a/crates/rpc/tests/device_room.rs b/crates/rpc/tests/device_room.rs index f39018021..3826e7f14 100644 --- a/crates/rpc/tests/device_room.rs +++ b/crates/rpc/tests/device_room.rs @@ -25,13 +25,14 @@ use tokio_tungstenite::tungstenite::handshake::server::{ Request as WsRequest, Response as WsResponse, }; +use zeron_crypto::channel::{ChannelIdentity, ChannelScope, PeerIdentity}; use zeron_rpc::device_room::{ CLIENT_CLOSED, CLIENT_GONE, HOST_CLOSED, HOST_OFFLINE, NUDGE_KIND, RELAY_KIND, }; use zeron_rpc::{ - DeviceFrameHeader, DeviceLink, HostRelay, HostRelayConfig, LinkCache, LinkCacheConfig, - RpcError, RpcReply, RpcService, StaticToken, TokenSource, decode_device_frame, - device_room_ws_url, encode_device_frame, methods, + ChannelAuthority, ChannelHost, ChannelLocal, DeviceFrameHeader, DeviceLink, HostRelay, + HostRelayConfig, LinkCache, LinkCacheConfig, RpcError, RpcReply, RpcService, StaticToken, + TokenSource, decode_device_frame, device_room_ws_url, encode_device_frame, methods, }; // --------------------------------------------------------------------------- @@ -50,6 +51,11 @@ struct RelayState { /// Zombie-path simulation: the host stays "connected" (no bounce) but /// client→host frames vanish — the 2026-08-19 dead edge↔host leg. blackhole_host_bound: bool, + /// Plaintext `rpc` frames the relay saw in either direction — the + /// leakage canary for the device-channel tests. + plaintext_rpc_frames: usize, + /// Every distinct frame kind the relay routed. + kinds_seen: Vec, } struct FakeRelay { @@ -99,6 +105,14 @@ impl FakeRelay { self.state.lock().expect("lock").blackhole_host_bound = on; } + fn plaintext_rpc_frames(&self) -> usize { + self.state.lock().expect("lock").plaintext_rpc_frames + } + + fn kinds_seen(&self) -> Vec { + self.state.lock().expect("lock").kinds_seen.clone() + } + /// Deliver a nudge frame to the connected host (the DO's /nudge live path). fn nudge(&self, chat_id: &str) { let header = DeviceFrameHeader::new(chat_id, NUDGE_KIND); @@ -186,7 +200,13 @@ async fn handle_socket(stream: tokio::net::TcpStream, state: Arc zeron_rpc::NudgeHandler { Arc::new(|_| {}) } +// --------------------------------------------------------------------------- +// Device-channel membership authority (the engine's vault, in miniature) +// --------------------------------------------------------------------------- + +const SCOPE: ChannelScope = ChannelScope { + vault_id: [7; 16], + generation: [8; 16], +}; + +/// One device's channel identity plus the members it currently trusts. +struct MemberAuthority { + device_id: [u8; 16], + secret: [u8; 32], + members: Mutex>, + required: bool, +} + +impl MemberAuthority { + fn new(tag: u8, required: bool) -> Arc { + Arc::new(Self { + device_id: [tag; 16], + secret: [tag ^ 0xa5; 32], + members: Mutex::new(Vec::new()), + required, + }) + } + + fn identity(&self) -> PeerIdentity { + PeerIdentity { + device_id: self.device_id, + static_key: ChannelIdentity::new(self.device_id, &self.secret) + .expect("identity") + .public_key(), + } + } + + fn trust(&self, other: &MemberAuthority) { + self.members.lock().expect("lock").push(other.identity()); + } + + fn revoke(&self, other: &MemberAuthority) { + let gone = other.identity(); + self.members.lock().expect("lock").retain(|m| *m != gone); + } +} + +impl ChannelAuthority for MemberAuthority { + fn required(&self) -> bool { + self.required + } + + fn local(&self) -> Result { + Ok(ChannelLocal { + identity: ChannelIdentity::new(self.device_id, &self.secret) + .map_err(|e| e.to_string())?, + scope: SCOPE, + }) + } + + fn accept(&self, peer: &PeerIdentity) -> bool { + self.members.lock().expect("lock").contains(peer) + } +} + +fn secured_host( + edge_url: &str, + authority: Arc, + service: Arc, +) -> HostRelay { + let mut config = relay_config(edge_url, 100); + config.channel = Some(ChannelHost { + authority, + service: service.clone(), + }); + HostRelay::spawn(config, service, noop_nudge()) +} + +fn secured_cache(edge_url: &str, authority: Arc) -> Arc { + let mut config = LinkCacheConfig::new(edge_url, Arc::new(StaticToken("test-user".into()))); + config.cooldown_base = Duration::from_millis(100); + config.cooldown_max = Duration::from_millis(400); + config.probe_timeout = Duration::from_millis(1_500); + config.channel = Some(authority); + LinkCache::new(config) +} + struct RecoveringToken { value: Mutex>, changes: tokio::sync::watch::Sender, @@ -452,6 +558,10 @@ async fn sign_out_closes_cached_peer_links() { .await .expect("link is live before sign-out"); + for _ in 0..32 { + zeron_sync::wake::notify_online(); + } + tokio::time::sleep(Duration::from_millis(20)).await; token.clear(); tokio::time::timeout(Duration::from_secs(5), async { @@ -689,6 +799,226 @@ async fn nudges_reach_the_host_callback() { assert_eq!(got, "chat-42"); } +// --------------------------------------------------------------------------- +// Authenticated device channel (RFC 0001 §10) +// --------------------------------------------------------------------------- + +/// Two mutually-trusting members: RPC and streams work end to end, and the +/// relay never sees a plaintext `rpc` frame — only handshake/ciphertext +/// (`chan`) and payload-less echoes. +#[tokio::test] +async fn device_channel_carries_rpc_with_no_plaintext_on_the_relay() { + let relay = FakeRelay::start().await; + let host_authority = MemberAuthority::new(0x11, true); + let client_authority = MemberAuthority::new(0x22, true); + host_authority.trust(&client_authority); + client_authority.trust(&host_authority); + let _host = secured_host( + &relay.edge_url(), + host_authority.clone(), + TestService::new("host-a"), + ); + relay.wait_host_connected().await; + + let links = secured_cache(&relay.edge_url(), client_authority.clone()); + let client = links.client("dev-a").await.expect("secured dial"); + let echoed = client + .call("Echo", serde_json::json!({ "secret": "plaintext canary" })) + .await + .expect("echo over the channel"); + assert_eq!(echoed["host"], "host-a"); + assert_eq!(echoed["params"]["secret"], "plaintext canary"); + + let mut items = client + .subscribe("Count", serde_json::json!({ "n": 3 })) + .await + .expect("stream over the channel"); + let mut seen = Vec::new(); + while let Some(v) = items.recv().await { + seen.push(v.as_u64().expect("number")); + } + assert_eq!(seen, vec![0, 1, 2]); + + // A frame past Noise's 64 KiB message cap is chunked and reassembled. + let big = "x".repeat(200_000); + let echoed = client + .call("Echo", serde_json::json!({ "big": big })) + .await + .expect("large echo"); + assert_eq!( + echoed["params"]["big"].as_str().map(str::len), + Some(200_000) + ); + + assert_eq!( + relay.plaintext_rpc_frames(), + 0, + "no plaintext RPC crossed the relay" + ); + let kinds = relay.kinds_seen(); + assert!(kinds.contains(&"chan".to_string()), "kinds: {kinds:?}"); + assert!( + kinds.iter().all(|k| k == "chan" || k == "echo"), + "unexpected frame kinds on the relay: {kinds:?}" + ); + + // The link authenticated the host's vault identity, not a relay id. + let url = device_room_ws_url(&relay.edge_url(), "dev-a", "client", Some("c-peer"), "t"); + let link = DeviceLink::connect_secured(&url, client_authority.as_ref()) + .await + .expect("direct secured link"); + assert_eq!(link.peer(), Some(host_authority.identity())); +} + +/// An enrolled host refuses plaintext: a legacy client's very first RPC +/// (the readiness probe) is answered with `encrypted_channel_required` +/// and the dial fails — no downgrade, no cached link. +#[tokio::test] +async fn enrolled_host_refuses_plaintext_clients() { + let relay = FakeRelay::start().await; + let host_authority = MemberAuthority::new(0x11, true); + let _host = secured_host( + &relay.edge_url(), + host_authority, + TestService::new("host-a"), + ); + relay.wait_host_connected().await; + + let links = cache(&relay.edge_url()); + let Err(err) = links.client("dev-a").await else { + panic!("plaintext dial must be refused"); + }; + assert!( + err.to_string().contains("encrypted_channel_required"), + "got: {err}" + ); +} + +/// Membership gates both directions: a host that does not list the caller +/// refuses it after message 3, and a caller that does not list the host +/// refuses BEFORE revealing its own identity (message 3 never leaves). +#[tokio::test] +async fn device_channel_refuses_non_members_on_either_side() { + let relay = FakeRelay::start().await; + let host_authority = MemberAuthority::new(0x11, true); + let member = MemberAuthority::new(0x22, true); + let stranger = MemberAuthority::new(0x33, true); + host_authority.trust(&member); + member.trust(&host_authority); + stranger.trust(&host_authority); + let _host = secured_host( + &relay.edge_url(), + host_authority.clone(), + TestService::new("host-a"), + ); + relay.wait_host_connected().await; + + // Stranger trusts the host, but the host does not list the stranger. + let url = device_room_ws_url( + &relay.edge_url(), + "dev-a", + "client", + Some("c-stranger"), + "t", + ); + let link = DeviceLink::connect_secured(&url, stranger.as_ref()).await; + let refused = match link { + Err(err) => err.to_string(), + Ok(link) => { + // The handshake may complete on the client before the host's + // verdict arrives; the first call then surfaces the rejection. + let outcome = link.client().call("Echo", serde_json::json!({})).await; + outcome + .expect_err("stranger must not get an answer") + .to_string() + } + }; + assert!( + refused.contains("channel_rejected") + || refused.contains("Closed") + || refused.contains("closed"), + "got: {refused}" + ); + + // A member that no longer trusts the host: refused locally, before message 3. + member.revoke(&host_authority); + let url = device_room_ws_url(&relay.edge_url(), "dev-a", "client", Some("c-member"), "t"); + let Err(err) = DeviceLink::connect_secured(&url, member.as_ref()).await else { + panic!("an untrusted host must not get a channel"); + }; + assert!(err.to_string().contains("peer refused"), "got: {err}"); + + assert_eq!(relay.plaintext_rpc_frames(), 0); +} + +/// A revocation that reaches the host mid-session ends it at the next +/// sealed frame: the client's call fails and the link is down. +#[tokio::test] +async fn device_channel_ends_when_membership_is_revoked() { + let relay = FakeRelay::start().await; + let host_authority = MemberAuthority::new(0x11, true); + let client_authority = MemberAuthority::new(0x22, true); + host_authority.trust(&client_authority); + client_authority.trust(&host_authority); + let _host = secured_host( + &relay.edge_url(), + host_authority.clone(), + TestService::new("host-a"), + ); + relay.wait_host_connected().await; + + let links = secured_cache(&relay.edge_url(), client_authority.clone()); + let client = links.client("dev-a").await.expect("secured dial"); + client + .call("Echo", serde_json::json!({})) + .await + .expect("live before revocation"); + + host_authority.revoke(&client_authority); + let err = client + .call("Echo", serde_json::json!({})) + .await + .expect_err("revoked peer gets no answer"); + assert!( + matches!(err, RpcError::Closed | RpcError::Transport(_)), + "got: {err}" + ); + // The cache noticed the link drop; a redial is refused at the handshake. + let redial = loop { + match links.client("dev-a").await { + Err(err) if err.to_string().contains("backing off") => { + tokio::time::sleep(Duration::from_millis(120)).await; + } + other => break other, + } + }; + assert!( + redial.is_err(), + "revoked member must not re-establish the channel" + ); +} + +/// A configured authority that is NOT yet enrolled keeps the plaintext +/// path: the vault's "no compatibility layer" rule is about enrolled +/// profiles, not about unenrolled ones losing remote control. +#[tokio::test] +async fn unenrolled_authority_keeps_plaintext_relay() { + let relay = FakeRelay::start().await; + let host_authority = MemberAuthority::new(0x11, false); + let client_authority = MemberAuthority::new(0x22, false); + let _host = secured_host( + &relay.edge_url(), + host_authority, + TestService::new("host-a"), + ); + relay.wait_host_connected().await; + + let links = secured_cache(&relay.edge_url(), client_authority); + let client = links.client("dev-a").await.expect("plaintext dial"); + assert!(client.call("Echo", serde_json::json!({})).await.is_ok()); + assert!(relay.plaintext_rpc_frames() > 0); +} + /// Live-edge variant: run the same host+client path through a real DeviceRoom DO. /// `ZERON_EDGE_WS=http://127.0.0.1:26640 cargo test -p zeron-rpc -- --ignored live_edge` /// (dev-mode edge; ZERON_EDGE_TOKEN defaults to a fixed dev user id). diff --git a/crates/sync/Cargo.toml b/crates/sync/Cargo.toml index b48a445b6..277c43eeb 100644 --- a/crates/sync/Cargo.toml +++ b/crates/sync/Cargo.toml @@ -18,6 +18,7 @@ rusqlite.workspace = true thiserror.workspace = true tracing.workspace = true uuid.workspace = true +zeron-crypto.workspace = true # NWPathMonitor bridge (net_path.rs) — the update handler is an ObjC block. [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/sync/examples/chat2_live.rs b/crates/sync/examples/chat2_live.rs index d522ef8bc..5e550bd99 100644 --- a/crates/sync/examples/chat2_live.rs +++ b/crates/sync/examples/chat2_live.rs @@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex}; use futures::future::BoxFuture; use loro::{ExportMode, LoroDoc, VersionVector}; use zeron_sync::SyncError; -use zeron_sync::chat_client::{ChatClient, ChatDocSink, CheckpointFetcher, RowImportOutcome}; +use zeron_sync::chat_client::{ApplyOutcome, ChatClient, ChatDocSink, CheckpointFetcher}; struct DocSink { doc: Mutex, @@ -24,21 +24,23 @@ struct DocSink { } impl ChatDocSink for DocSink { - fn apply_row(&self, bytes: &[u8], cursor: u64) -> RowImportOutcome { + fn apply_row(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome { let doc = self.doc.lock().unwrap(); if doc.import(bytes).expect("row import").pending.is_some() { - return RowImportOutcome::PendingDependencies; + return ApplyOutcome::PendingDependencies; } self.cursor.store(cursor, Relaxed); self.rows_applied.fetch_add(1, Relaxed); - RowImportOutcome::Applied + ApplyOutcome::Applied } - fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> Result<(), String> { + fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome { let doc = self.doc.lock().unwrap(); - doc.import(bytes).map_err(|e| e.to_string())?; + if doc.import(bytes).is_err() { + return ApplyOutcome::StorageFailed; + } self.cursor.store(cursor, Relaxed); self.checkpoint_applied.store(true, Relaxed); - Ok(()) + ApplyOutcome::Applied } fn contains_frontier(&self, frontier: &[u8]) -> bool { if frontier.is_empty() { diff --git a/crates/sync/src/chat_client.rs b/crates/sync/src/chat_client.rs index 3d7144ec1..795daa9d6 100644 --- a/crates/sync/src/chat_client.rs +++ b/crates/sync/src/chat_client.rs @@ -102,32 +102,74 @@ pub enum ChatEvent { /// next posts a checkpoint — the C3 host should treat this event as a /// checkpoint trigger, not a shrug. PushRejected, + /// A remote row/checkpoint could not be verified or opened; the cursor + /// holds and inbound application is paused until [`ChatClient::resume`]. + /// Pushes and presence continue. The host decides the recovery action + /// (refresh keys, require upgrade, surface "sync paused"). + SyncPaused(ApplyOutcome), + /// Inbound application resumed (a backfill from the honest cursor follows). + SyncResumed, } // ── engine-facing traits ──────────────────────────────────────────────────── -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RowImportOutcome { +/// The explicit verified/durable result of applying remote bytes (RFC 0001 +/// §8). Only `Applied` lets the cursor advance; every other outcome holds +/// the honest cursor so the row is re-delivered once the cause is fixed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ApplyOutcome { + /// Doc and cursor committed atomically (or the row was a tolerated + /// no-op under the sink's own policy). + #[default] Applied, + /// The row is authentic-looking but this device lacks its key/epoch: + /// keep the data, refresh keys, do not advance. + KeyUnavailable, + /// Signature/context/AEAD verification failed: pause, do not advance. + AuthenticationFailed, + /// Unknown format/version: an upgrade is required; do not advance. + Unsupported, + /// Import or storage failed transiently: preserve last good state and + /// let the transport retry (redial / refetch). + StorageFailed, /// The update is buffered in memory until missing causal history arrives. /// It is not represented in an exported snapshot yet. PendingDependencies, } +impl ApplyOutcome { + /// Outcomes that pause the session until the host resolves them (a + /// redial cannot fix them and would loop). + pub fn pauses(self) -> bool { + matches!( + self, + Self::KeyUnavailable | Self::AuthenticationFailed | Self::Unsupported + ) + } +} + /// Where remote bytes land. The engine implements this over its doc handle; /// every method persists doc content AND the room cursor in one transaction /// (`DocsStore::save_snapshot_with_cursor`) so they can never diverge. pub trait ChatDocSink: Send + Sync + 'static { /// Import one remote update row with a proposed contiguous cursor. A /// pending import must not persist that cursor; the client repairs it. - fn apply_row(&self, bytes: &[u8], cursor: u64) -> RowImportOutcome; + /// Import one remote update row; `cursor` is the row's seq. The cursor + /// is only committed by the client when this returns `Applied`. + fn apply_row(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome; /// Replace/merge from a checkpoint blob; `cursor` is its checkpointSeq. - fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> Result<(), String>; + fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome; /// Client-side precision (replaces the server VV diff): is the server - /// checkpoint's frontier already contained in the local doc? + /// checkpoint's frontier already contained in the local doc? A frontier + /// that cannot be verified must answer `false` (fetch), never `true`. fn contains_frontier(&self, frontier: &[u8]) -> bool; /// An own-write ack advanced the cursor with no content change. fn advance_cursor(&self, cursor: u64); + /// The server durably accepted `batch_id` (sealed outbox entries may be + /// retired now). Default: nothing to retire. + fn acknowledged(&self, _batch_id: &str) {} + /// The server permanently rejected `batch_id`. + fn rejected(&self, _batch_id: &str) {} } /// `GET /chat2/{chatId}/checkpoint` over HTTP. Implementations should resume @@ -313,11 +355,24 @@ struct Shared { needs_checkpoint: bool, /// Prevent an overlapping HTTP/socket catch-up from clearing a newer gap. causal_gap_generation: u64, + /// Inbound application is paused (RFC 0001 §8): a row or checkpoint + /// returned a non-`Applied` outcome that a redial cannot fix. While set, + /// remote rows are dropped (they sit above the honest cursor and are + /// refetched on resume) and pushes continue. + paused: Option, } -fn apply_remote_row(shared: &Mutex, sink: &dyn ChatDocSink, bytes: &[u8], seq: u64) { +fn apply_remote_row( + shared: &Mutex, + sink: &dyn ChatDocSink, + bytes: &[u8], + seq: u64, +) -> ApplyOutcome { let cursor = { let mut shared = lock(shared); + if let Some(outcome) = shared.paused { + return outcome; + } if seq > shared.cursor.saturating_add(1) { shared.gap_repair = true; shared.cursor @@ -330,8 +385,8 @@ fn apply_remote_row(shared: &Mutex, sink: &dyn ChatDocSink, bytes: &[u8] let outcome = sink.apply_row(bytes, cursor); let mut shared = lock(shared); match outcome { - RowImportOutcome::Applied => shared.cursor = shared.cursor.max(cursor), - RowImportOutcome::PendingDependencies => { + ApplyOutcome::Applied => shared.cursor = shared.cursor.max(cursor), + ApplyOutcome::PendingDependencies => { shared.needs_checkpoint = true; shared.causal_gap_generation = shared.causal_gap_generation.wrapping_add(1); tracing::warn!( @@ -340,7 +395,10 @@ fn apply_remote_row(shared: &Mutex, sink: &dyn ChatDocSink, bytes: &[u8] "chat2: missing causal history; holding cursor and refreshing checkpoint" ); } + ApplyOutcome::StorageFailed => shared.gap_repair = true, + _ => {} } + outcome } /// `zeron sync` surface (plan: cursor / headSeq / floorLag / pendingPushes). @@ -368,6 +426,8 @@ pub struct ChatStatsSnapshot { /// Times a hello found the server behind our cursor (room reset/wiped). /// Nonzero means the host owes the room a re-seed checkpoint. pub server_resets: u64, + /// Inbound application is paused on this outcome (see `ChatEvent::SyncPaused`). + pub paused: Option, } fn lock(mutex: &Mutex) -> MutexGuard<'_, T> { @@ -384,6 +444,7 @@ pub struct ChatClient { nudge: mpsc::Sender<()>, probe: mpsc::Sender<()>, redial: mpsc::Sender<()>, + resume: mpsc::Sender<()>, presence_out: mpsc::Sender<(i64, Vec)>, flags: Arc, task: Option>, @@ -505,6 +566,7 @@ impl ChatClient { let (nudge_tx, nudge_rx) = mpsc::channel(1); let (probe_tx, probe_rx) = mpsc::channel(1); let (redial_tx, redial_rx) = mpsc::channel(1); + let (resume_tx, resume_rx) = mpsc::channel(1); let (presence_tx, presence_rx) = mpsc::channel(4); let shared = Arc::new(Mutex::new(Shared { cursor: initial_cursor, @@ -524,6 +586,7 @@ impl ChatClient { nudge_rx, probe_rx, redial_rx, + resume_rx, presence_rx, flags: flags.clone(), resumed: false, @@ -541,6 +604,7 @@ impl ChatClient { nudge: nudge_tx, probe: probe_tx, redial: redial_tx, + resume: resume_tx, presence_out: presence_tx, flags, task: Some(task), @@ -589,6 +653,37 @@ impl ChatClient { let _ = self.nudge.try_send(()); } + /// Queue an already-sealed, already-persisted batch under ITS batch id + /// (the encrypted outbox contract: identical bytes on every retry; the + /// sink's `acknowledged` retires the durable copy). Duplicate ids are + /// ignored so an outbox replay after reconnect cannot double-queue. + pub fn enqueue_sealed(&self, batch_id: String, bytes: Vec) { + if bytes.len() > MAX_PUSH_BYTES { + use std::sync::atomic::Ordering::Relaxed; + tracing::error!( + bytes = bytes.len(), + "chat2: sealed batch exceeds the row cap" + ); + self.flags.rejected.fetch_add(1, Relaxed); + let _ = self.events.send(ChatEvent::PushRejected); + return; + } + { + let mut shared = lock(&self.shared); + if shared.pending.iter().any(|p| p.batch_id == batch_id) { + return; + } + shared.pending.push_back(PendingPush { batch_id, bytes }); + } + let _ = self.nudge.try_send(()); + } + + /// The host resolved whatever paused inbound application (keys arrived, + /// policy refreshed): clear the pause and backfill from the honest cursor. + pub fn resume(&self) { + let _ = self.resume.try_send(()); + } + /// Publish this device's presence beat with an opaque payload (cursor /// positions etc. — relayed verbatim, never stored). pub fn send_presence(&self, at: i64, payload: Vec) { @@ -649,6 +744,7 @@ impl ChatClient { disconnects: self.flags.disconnects.load(Relaxed), rejected: self.flags.rejected.load(Relaxed), server_resets: self.flags.server_resets.load(Relaxed), + paused: shared.paused, } } @@ -683,6 +779,7 @@ struct Actor { nudge_rx: mpsc::Receiver<()>, probe_rx: mpsc::Receiver<()>, redial_rx: mpsc::Receiver<()>, + resume_rx: mpsc::Receiver<()>, presence_rx: mpsc::Receiver<(i64, Vec)>, flags: Arc, /// Once-per-actor cursor amnesty (see run_session): a cursor above the @@ -1035,26 +1132,38 @@ impl Actor { } } }; - if let Err(err) = self.sink.apply_checkpoint(&bytes, state.checkpoint_seq) { - tracing::warn!(error = %err, "chat2: checkpoint import failed"); - return SessionEnd::Reconnect; + match self.sink.apply_checkpoint(&bytes, state.checkpoint_seq) { + ApplyOutcome::Applied => { + let mut shared = lock(&self.shared); + shared.cursor = shared.cursor.max(state.checkpoint_seq); + drop(shared); + let _ = self.events.send(ChatEvent::Applied); + } + ApplyOutcome::StorageFailed | ApplyOutcome::PendingDependencies => { + tracing::warn!("chat2: checkpoint import failed"); + return SessionEnd::Reconnect; + } + outcome => { + // Unverifiable checkpoint: a redial would refetch the + // same bytes forever. Pause instead; the host resolves + // keys/policy and calls `resume`, which refetches. + self.pause(outcome); + lock(&self.shared).needs_checkpoint = true; + } } - let mut shared = lock(&self.shared); - shared.cursor = shared.cursor.max(state.checkpoint_seq); - drop(shared); - let _ = self.events.send(ChatEvent::Applied); } // A new full catch-up may resolve a prior causal gap. Imports below // re-arm repair if any history is still missing. { let mut shared = lock(&self.shared); - if shared.causal_gap_generation == repair_generation { + if shared.paused.is_none() && shared.causal_gap_generation == repair_generation { shared.needs_checkpoint = false; } } // Frames buffered during the fetch replay first, then the live socket // finishes the backfill — one pass, same ROWS_DONE terminator either - // way. + // way. (While paused, rows are dropped by `handle_frame` and the + // server still terminates the backfill with ROWS_DONE.) let mut head_seq: Option = None; for frame in buffered.drain(..) { if head_seq.is_none() && frame.kind == frame_type::ROWS_DONE { @@ -1167,6 +1276,18 @@ impl Actor { tracing::info!("chat2: redial requested"); return SessionEnd::Reconnect; } + _ = self.resume_rx.recv() => { + let was_paused = lock(&self.shared).paused.take().is_some(); + if was_paused { + tracing::info!("chat2: inbound application resumed; backfilling"); + let _ = self.events.send(ChatEvent::SyncResumed); + lock(&self.shared).gap_repair = true; + gap_repairs = 0; + if !self.maybe_repair_gap(&mut pipe, &mut gap_repairs).await { + return SessionEnd::Reconnect; + } + } + } // Transient (quota) rejection: probe with the HEAD batch on // a short clock (see `Shared::quota_blocked`); acks re-arm // the clock so the queue drains one-per-grant. @@ -1195,6 +1316,17 @@ impl Actor { } } + /// Enter the paused state on a non-`Applied` outcome (idempotent). + fn pause(&self, outcome: ApplyOutcome) { + let mut shared = lock(&self.shared); + if shared.paused.is_none() { + shared.paused = Some(outcome); + drop(shared); + tracing::warn!(?outcome, "chat2: inbound application paused; cursor held"); + let _ = self.events.send(ChatEvent::SyncPaused(outcome)); + } + } + async fn send_probe( &self, pipe: &mut BinPipe, @@ -1261,7 +1393,11 @@ impl Actor { if let (Some(b), Some(seq)) = (v["batchId"].as_str(), v["seq"].as_u64()) { let mut sh = lock(&shared); + let retired = sh.pending.iter().any(|p| p.batch_id == b); sh.pending.retain(|p| p.batch_id != b); + if retired { + sink.acknowledged(b); + } // Contiguity rule (see handle_frame ACK): an // own-push ack proves the server has rows up // to `seq`, not that WE have the interleaved @@ -1325,16 +1461,31 @@ impl Actor { let contained = state.checkpoint_size == 0 || (!repair_causal_history && sink.contains_frontier(&state_frame.payload)); let plan = plan_catch_up(cursor, &state, contained); - if let CatchUpPlan::CheckpointThenRows { .. } = plan { + if let CatchUpPlan::CheckpointThenRows { after } = plan { let fetched = tokio::time::timeout(CHECKPOINT_FETCH_DEADLINE, fetcher.fetch()).await; match fetched { Ok(Ok(bytes)) => { - if sink.apply_checkpoint(&bytes, state.checkpoint_seq).is_err() { - busy.store(false, Relaxed); - return; + match sink.apply_checkpoint(&bytes, state.checkpoint_seq) { + ApplyOutcome::Applied => { + let mut sh = lock(&shared); + sh.cursor = sh.cursor.max(after); + drop(sh); + let _ = events.send(ChatEvent::Applied); + } + outcome => { + if outcome.pauses() { + let mut sh = lock(&shared); + if sh.paused.is_none() { + sh.paused = Some(outcome); + drop(sh); + let _ = events.send(ChatEvent::SyncPaused(outcome)); + } + } + busy.store(false, Relaxed); + return; + } } - let _ = events.send(ChatEvent::Applied); } _ => { busy.store(false, Relaxed); @@ -1372,8 +1523,18 @@ impl Actor { // contiguous — but hold the rule anyway: a jump // (trimmed log, server surprise) must not stamp the // cursor over rows the doc never saw. - apply_remote_row(&shared, sink.as_ref(), &frame.payload, row.seq); - applied = true; + match apply_remote_row(&shared, sink.as_ref(), &frame.payload, row.seq) { + ApplyOutcome::Applied => applied = true, + outcome => { + let mut sh = lock(&shared); + if outcome.pauses() && sh.paused.is_none() { + sh.paused = Some(outcome); + drop(sh); + let _ = events.send(ChatEvent::SyncPaused(outcome)); + } + break; + } + } } frame_type::ROWS_DONE => {} _ => {} @@ -1394,6 +1555,9 @@ impl Actor { const MAX_GAP_REPAIRS_PER_SESSION: u32 = 3; let (repair, after) = { let mut shared = lock(&self.shared); + if shared.paused.is_some() { + return true; + } if shared.needs_checkpoint { // A row backfill cannot restore dependencies already trimmed // into the checkpoint. Redial through the bounded backoff and @@ -1465,15 +1629,31 @@ impl Actor { // gap means rows we never received (live broadcast mid-join) // — apply the bytes (loro parks dependents harmlessly), keep // the honest cursor, and ask for a backfill repair. - apply_remote_row(&self.shared, self.sink.as_ref(), &frame.payload, row.seq); - let _ = self.events.send(ChatEvent::Applied); + if lock(&self.shared).paused.is_some() { + return true; // above the honest cursor; refetched on resume + } + match apply_remote_row(&self.shared, self.sink.as_ref(), &frame.payload, row.seq) { + ApplyOutcome::Applied => { + let _ = self.events.send(ChatEvent::Applied); + } + outcome => { + // The row did not land: the cursor may not claim it. + if outcome.pauses() { + self.pause(outcome); + } + } + } } frame_type::ACK => { let Ok(ack) = serde_json::from_value::(frame.header) else { return false; }; let mut shared = lock(&self.shared); + let retired = shared.pending.iter().any(|p| p.batch_id == ack.batch_id); shared.pending.retain(|p| p.batch_id != ack.batch_id); + if retired { + self.sink.acknowledged(&ack.batch_id); + } // Same contiguity rule as ROW: our own batch landing at // `seq` proves rows up to seq exist server-side, not that we // HAVE the interleaved ones from other devices. @@ -1527,7 +1707,11 @@ impl Actor { // it replays on every nudge/reconnect forever — the // wedge class this design exists to kill. The ops stay // in the local doc and travel with the next checkpoint. - "too_large" | "empty" | "bad_push" if !batch_id.is_empty() => { + // `plaintext_rejected`: the relay enforces ciphertext + // framing on encrypted rooms; a batch that failed it + // must never be replayed (RFC 0001 §12.2). + "too_large" | "empty" | "bad_push" | "plaintext_rejected" + if !batch_id.is_empty() => { let mut shared = lock(&self.shared); let before = shared.pending.len(); shared.pending.retain(|p| p.batch_id != batch_id); @@ -1540,6 +1724,7 @@ impl Actor { "chat2: batch permanently rejected — retired \ from the replay queue" ); + self.sink.rejected(batch_id); let _ = self.events.send(ChatEvent::PushRejected); } } diff --git a/crates/sync/src/chat_client/tests.rs b/crates/sync/src/chat_client/tests.rs index 827db16dc..18ebca95a 100644 --- a/crates/sync/src/chat_client/tests.rs +++ b/crates/sync/src/chat_client/tests.rs @@ -54,26 +54,32 @@ struct RecordingSink { /// Global apply order across rows and checkpoints — the overlap test /// pins "checkpoint imports before any row that buffered during it". ops: Mutex>, + /// Outcome every `apply_row` answers (default `Applied`). + row_outcome: Mutex, + acked: Mutex>, } impl ChatDocSink for RecordingSink { - fn apply_row(&self, bytes: &[u8], cursor: u64) -> RowImportOutcome { + fn apply_row(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome { if self .pending_until_checkpoint .load(std::sync::atomic::Ordering::Relaxed) { - return RowImportOutcome::PendingDependencies; + return ApplyOutcome::PendingDependencies; } lock(&self.rows).push((bytes.to_vec(), cursor)); lock(&self.ops).push(format!("row@{cursor}")); - RowImportOutcome::Applied + *lock(&self.row_outcome) } - fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> Result<(), String> { + fn apply_checkpoint(&self, bytes: &[u8], cursor: u64) -> ApplyOutcome { self.pending_until_checkpoint .store(false, std::sync::atomic::Ordering::Relaxed); lock(&self.checkpoints).push((bytes.to_vec(), cursor)); lock(&self.ops).push(format!("ckpt@{cursor}")); - Ok(()) + ApplyOutcome::Applied + } + fn acknowledged(&self, batch_id: &str) { + lock(&self.acked).push(batch_id.to_string()); } fn contains_frontier(&self, _frontier: &[u8]) -> bool { self.frontier_contained @@ -101,14 +107,14 @@ impl CheckpointFetcher for FixedFetcher { // ── server-side script helpers ────────────────────────────────────────────── async fn expect_kind(end: &mut ServerEnd, kind: u8) -> wire::WireFrame { - loop { - let bytes = end.rx.recv().await.expect("client hung up"); - let frame = decode(&bytes).expect("client sent undecodable frame"); - if frame.kind == kind { - return frame; - } - panic!("expected frame {kind:#x}, got {:#x}", frame.kind); - } + let bytes = end.rx.recv().await.expect("client hung up"); + let frame = decode(&bytes).expect("client sent undecodable frame"); + assert!( + frame.kind == kind, + "expected frame {kind:#x}, got {:#x}", + frame.kind + ); + frame } async fn send(end: &ServerEnd, kind: u8, header: serde_json::Value, payload: &[u8]) { @@ -1531,3 +1537,147 @@ async fn http_catchup_crosses_a_contained_checkpoint_and_repairs_causal_gaps() { client.shutdown().await; } } + +// ── verified apply outcomes (RFC 0001 §8) ─────────────────────────────────── + +/// A row the sink cannot verify/open must NOT advance the cursor: the +/// session pauses (no redial loop), later rows are dropped above the honest +/// cursor, and `resume` backfills from that cursor once the host fixed keys. +#[tokio::test(start_paused = true)] +async fn unverifiable_row_holds_cursor_pauses_and_resume_backfills() { + let (pipe, mut end) = pipe_pair(); + let sink = Arc::new(RecordingSink::default()); + *lock(&sink.row_outcome) = ApplyOutcome::KeyUnavailable; + let (fetch, _) = fetcher(b""); + let rows = vec![(1, "dev-b", vec![0xaa]), (2, "dev-b", vec![0xbb])]; + let rows_again = rows.clone(); + + let server = tokio::spawn(async move { + serve_join( + &mut end, + serde_json::json!({"headSeq": 2, "seqFloor": 0, "checkpointSeq": 0, + "checkpointSize": 0, "rowCount": 2, "rowBytes": 64}), + &[], + rows, + false, + ) + .await; + // The resume backfill: a rowsReq from the honest cursor (0). + let req = expect_kind(&mut end, frame_type::ROWS_REQ).await; + assert_eq!(req.header["after"], 0); + for (seq, device, bytes) in rows_again { + send( + &end, + frame_type::ROW, + serde_json::json!({"seq": seq, "device": device, "batchId": format!("b{seq}")}), + &bytes, + ) + .await; + } + send( + &end, + frame_type::ROWS_DONE, + serde_json::json!({"headSeq": 2}), + &[], + ) + .await; + end + }); + + let client = ChatClient::connect_with_tuned( + connector(vec![pipe]), + sink.clone(), + fetch, + "dev-a", + 0, + ChatTuning::default(), + ) + .await + .expect("join succeeds even while paused"); + let mut events = client.events(); + let stats = client.stats(); + assert_eq!(stats.cursor, 0, "an unopened row never advances the cursor"); + assert_eq!(stats.paused, Some(ApplyOutcome::KeyUnavailable)); + assert_eq!( + lock(&sink.rows).len(), + 1, + "row 2 is dropped above the honest cursor, not offered while paused" + ); + assert!(lock(&sink.cursor_advances).is_empty()); + + // Keys arrive: resume backfills from 0 and both rows apply. + *lock(&sink.row_outcome) = ApplyOutcome::Applied; + lock(&sink.rows).clear(); + client.resume(); + server.await.unwrap(); + let mut saw_resumed = false; + for _ in 0..8 { + match tokio::time::timeout(Duration::from_secs(1), events.recv()).await { + Ok(Ok(ChatEvent::SyncResumed)) => saw_resumed = true, + Ok(Ok(_)) => {} + _ => break, + } + if lock(&sink.rows).len() == 2 { + break; + } + } + assert!(saw_resumed, "resume is announced"); + assert_eq!(*lock(&sink.rows), vec![(vec![0xaa], 1), (vec![0xbb], 2)]); + let stats = client.stats(); + assert_eq!(stats.cursor, 2); + assert_eq!(stats.paused, None); + client.shutdown().await; +} + +/// Pre-sealed outbox batches keep their durable ids on the wire, are never +/// queued twice, and their acks reach the sink so the outbox can retire them. +#[tokio::test(start_paused = true)] +async fn sealed_batches_keep_their_ids_and_acks_reach_the_sink() { + let (pipe, mut end) = pipe_pair(); + let sink = Arc::new(RecordingSink::default()); + let (fetch, _) = fetcher(b""); + + let server = tokio::spawn(async move { + serve_join(&mut end, empty_state_json(), &[], vec![], false).await; + let push = expect_kind(&mut end, frame_type::PUSH).await; + assert_eq!(push.header["batchId"], "outbox-7"); + assert_eq!(push.payload, vec![7, 7, 7]); + send( + &end, + frame_type::ACK, + serde_json::json!({"batchId": "outbox-7", "seq": 1}), + &[], + ) + .await; + end + }); + + let client = ChatClient::connect_with_tuned( + connector(vec![pipe]), + sink.clone(), + fetch, + "dev-a", + 0, + ChatTuning::default(), + ) + .await + .expect("join"); + client.enqueue_sealed("outbox-7".into(), vec![7, 7, 7]); + client.enqueue_sealed("outbox-7".into(), vec![7, 7, 7]); + assert_eq!( + client.stats().pending_pushes, + 1, + "duplicate ids are not queued twice" + ); + server.await.unwrap(); + for _ in 0..20 { + if client.stats().pending_pushes == 0 { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + assert_eq!(client.stats().pending_pushes, 0); + assert_eq!(*lock(&sink.acked), vec!["outbox-7".to_string()]); + assert_eq!(*lock(&sink.cursor_advances), vec![1]); + client.shutdown().await; +} diff --git a/crates/sync/src/lib.rs b/crates/sync/src/lib.rs index 9e9bc42e2..eea0fb2fe 100644 --- a/crates/sync/src/lib.rs +++ b/crates/sync/src/lib.rs @@ -23,7 +23,8 @@ pub use chat_client::{ ChatClient, ChatDocSink, ChatEvent, ChatStatsSnapshot, ChatTuning, CheckpointFetcher, }; pub use registry::{ - ReconnectState, RegistryClient, RegistryEvent, RegistryTransport, RegistryTuning, + FieldOpenFailure, ReconnectState, RegistryClient, RegistryCodec, RegistryEvent, + RegistryTransport, RegistryTuning, open_rows, seal_ops, }; -pub use store::{DocsStore, StoreError}; +pub use store::{DocsStore, MAX_ENCRYPTED_OUTBOX_BYTES, PendingEncryptedBatch, StoreError}; pub use types::{RoomStatsSnapshot, StaticUrl, SyncError, UrlProvider}; diff --git a/crates/sync/src/registry.rs b/crates/sync/src/registry.rs index 2345fe8cb..14746b715 100644 --- a/crates/sync/src/registry.rs +++ b/crates/sync/src/registry.rs @@ -25,7 +25,8 @@ use tokio::sync::{broadcast, mpsc, oneshot, watch}; use tokio_tungstenite::tungstenite::Message as WsMessage; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; -use zeron_doc::{PendingBatch, RegistryDoc, RegistryRow, StateOutcome}; +use zeron_doc::registry::{OpKind, hlc_newer}; +use zeron_doc::{PendingBatch, RegistryDoc, RegistryRow, RowOp, StateOutcome}; use crate::types::{RoomStatsSnapshot, StaticUrl, SyncError, UrlProvider}; @@ -164,6 +165,185 @@ pub trait RegistryTransport: Send + Sync + 'static { fn push(&self, body: String) -> BoxFuture<'static, Result>; } +/// Why an inbound field could not be opened (RFC 0001 §9). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldOpenFailure { + /// The key/epoch is not held yet: withhold the row and hold the cursor. + KeyUnavailable, + /// Not authentic under pinned membership (or plaintext where ciphertext + /// is required): drop the field, never the row's other verified fields. + Rejected, +} + +/// Per-field seal/open boundary for an encrypted profile's registry. The +/// server keeps merging by row/field/clock; only VALUES cross this seam, and +/// each sealed value binds its row, field, and clock inside the +/// authenticated plaintext so a relay cannot move one between slots. +pub trait RegistryCodec: Send + Sync + 'static { + /// Seal one outbound write (`value == Null` = an authenticated field + /// deletion marker). `Err` keeps the batch pending (keys not ready). + fn seal_field( + &self, + kind: &str, + id: &str, + field: &str, + hlc: &str, + value: &serde_json::Value, + ) -> Result; + /// Open one inbound value. `Ok(None)` is an authenticated deletion. + fn open_field( + &self, + kind: &str, + id: &str, + field: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result, FieldOpenFailure>; + /// Seal the lifecycle proof for a row deletion at `hlc` (RFC 0001 §9): + /// the authenticated plaintext names the row and its tombstone clock, + /// so a relay can neither invent a tombstone nor move one between rows. + fn seal_lifecycle(&self, kind: &str, id: &str, hlc: &str) + -> Result; + /// Verify a tombstone's proof against the slot the server filed it under. + fn open_lifecycle( + &self, + kind: &str, + id: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result<(), FieldOpenFailure>; +} + +/// Seal every write in `ops` for the wire. `None` when any field cannot be +/// sealed yet (the whole batch stays queued — never a half-sealed batch). +pub fn seal_ops(codec: &dyn RegistryCodec, ops: &[RowOp]) -> Option> { + let mut out = Vec::with_capacity(ops.len()); + for op in ops { + let mut sealed = op.clone(); + if let Some(set) = &op.set { + let mut wire = std::collections::BTreeMap::new(); + for (field, value) in set { + let clock = op + .clocks + .as_ref() + .and_then(|c| c.get(field)) + .map_or(op.hlc.as_str(), String::as_str); + match codec.seal_field(&op.kind, &op.id, field, clock, value) { + Ok(value) => { + wire.insert(field.clone(), value); + } + Err(reason) => { + tracing::info!(reason, "registry: field sealing deferred"); + return None; + } + } + } + sealed.set = Some(wire); + } + if op.op == OpKind::Delete { + match codec.seal_lifecycle(&op.kind, &op.id, &op.hlc) { + Ok(proof) => sealed.proof = Some(proof), + Err(reason) => { + tracing::info!(reason, "registry: lifecycle sealing deferred"); + return None; + } + } + } + out.push(sealed); + } + Some(out) +} + +/// Open every field of `rows`. Rows with a field whose key is not held are +/// withheld (returned count); rejected fields are dropped from their row. +/// +/// Row lifecycle (RFC 0001 §9): a tombstone is accepted only with a verified +/// proof for exactly this row and clock, and only if it is causally newer +/// than the verified baseline; a live row over a verified tombstone must +/// carry at least one verified field newer than the tombstone. Anything +/// else is dropped — a relay can neither delete nor resurrect a row. +pub fn open_rows( + codec: &dyn RegistryCodec, + rows: Vec, + baseline: &RegistryDoc, +) -> (Vec, usize) { + let mut out = Vec::with_capacity(rows.len()); + let mut withheld = 0; + 'rows: for mut row in rows { + let previous = baseline.authoritative_row(&row.kind, &row.id); + if row.deleted { + let (Some(hlc), Some(proof)) = (row.del_hlc.as_deref(), row.del_proof.as_ref()) else { + tracing::warn!(kind = %row.kind, "registry: tombstone without proof; dropped"); + continue; + }; + match codec.open_lifecycle(&row.kind, &row.id, hlc, proof) { + Ok(()) => {} + Err(FieldOpenFailure::KeyUnavailable) => { + withheld += 1; + continue; + } + Err(FieldOpenFailure::Rejected) => { + tracing::warn!(kind = %row.kind, "registry: tombstone proof rejected; dropped"); + continue; + } + } + let newer = match previous { + None => true, + Some(previous) if previous.deleted => hlc_newer(hlc, previous.del_hlc.as_deref()), + Some(previous) => hlc_newer(hlc, previous.max_clock()), + }; + if !newer { + tracing::warn!(kind = %row.kind, "registry: stale tombstone; dropped"); + continue; + } + row.fields.clear(); + row.clocks.clear(); + out.push(row); + continue; + } + let revives = previous.filter(|p| p.deleted).and_then(|p| p.del_hlc.clone()); + let mut fields = previous.map(|r| r.fields.clone()).unwrap_or_default(); + let mut clocks = previous.map(|r| r.clocks.clone()).unwrap_or_default(); + for (field, wire) in &row.fields { + let clock = row.clocks.get(field).map_or("", String::as_str); + match codec.open_field(&row.kind, &row.id, field, clock, wire) { + Ok(value) => { + if clocks.get(field).is_some_and(|old| old.as_str() >= clock) { + continue; + } + clocks.insert(field.clone(), clock.to_string()); + match value { + Some(value) => { + fields.insert(field.clone(), value); + } + None => { + fields.remove(field); + } + } + } + Err(FieldOpenFailure::KeyUnavailable) => { + withheld += 1; + continue 'rows; + } + Err(FieldOpenFailure::Rejected) => { + tracing::warn!(kind = %row.kind, field, "registry: field rejected; dropped"); + } + } + } + if let Some(del_hlc) = &revives + && !clocks.values().any(|clock| clock > del_hlc) + { + tracing::warn!(kind = %row.kind, "registry: revival without newer evidence; dropped"); + continue; + } + row.fields = fields; + row.clocks = clocks; + row.del_proof = None; + out.push(row); + } + (out, withheld) +} + struct WsTextConnector { url: Arc, } @@ -351,7 +531,7 @@ impl RegistryClient { tuning: RegistryTuning, ) -> Result { let connector = Arc::new(WsTextConnector { url: provider }); - Self::connect_with_transport(connector, doc, device_id, tuning, None).await + Self::connect_with_transport(connector, doc, device_id, tuning, None, None).await } /// Connect with a plain-HTTPS pull/push seam alongside the socket (the @@ -366,7 +546,48 @@ impl RegistryClient { transport: Arc, ) -> Result { let connector = Arc::new(WsTextConnector { url: provider }); - Self::connect_with_transport(connector, doc, device_id, tuning, Some(transport)).await + Self::connect_with_transport(connector, doc, device_id, tuning, Some(transport), None).await + } + + /// [`Self::connect_via_transport`] for an encrypted profile: every + /// pushed value is sealed and every received value opened through `codec`. + pub async fn connect_via_transport_with_codec( + provider: Arc, + doc: Arc>, + device_id: &str, + tuning: RegistryTuning, + transport: Arc, + codec: Arc, + ) -> Result { + let connector = Arc::new(WsTextConnector { url: provider }); + Self::connect_with_transport( + connector, + doc, + device_id, + tuning, + Some(transport), + Some(codec), + ) + .await + } + + /// Socket-only encrypted join (tests, dev bearers). + pub async fn connect_via_codec( + provider: Arc, + doc: Arc>, + device_id: &str, + codec: Arc, + ) -> Result { + let connector = Arc::new(WsTextConnector { url: provider }); + Self::connect_with_transport( + connector, + doc, + device_id, + RegistryTuning::default(), + None, + Some(codec), + ) + .await } pub(crate) async fn connect_with_tuned( @@ -375,7 +596,7 @@ impl RegistryClient { device_id: &str, tuning: RegistryTuning, ) -> Result { - Self::connect_with_transport(connector, doc, device_id, tuning, None).await + Self::connect_with_transport(connector, doc, device_id, tuning, None, None).await } pub(crate) async fn connect_with_transport( @@ -384,6 +605,7 @@ impl RegistryClient { device_id: &str, tuning: RegistryTuning, transport: Option>, + codec: Option>, ) -> Result { let (events, _) = broadcast::channel(256); let (shutdown_tx, shutdown_rx) = watch::channel(false); @@ -409,6 +631,7 @@ impl RegistryClient { presence: presence.clone(), stats: stats.clone(), transport, + codec, sync_busy: Arc::new(std::sync::atomic::AtomicBool::new(false)), }; let task = tokio::spawn(actor.run(ready_tx)); @@ -526,10 +749,36 @@ struct Actor { stats: Arc, /// Plain-HTTPS pull/push (None = socket-only: tests, dev bearers). transport: Option>, + /// Encrypted profile: per-field seal/open (None = plaintext registry). + codec: Option>, /// One offline sync in flight at a time. sync_busy: Arc, } +/// Open inbound rows through the codec (identity without one) and hold the +/// doc's cursor at `previous` when any row was withheld for missing keys. +fn apply_opened( + codec: Option<&Arc>, + doc: &Mutex, + rows: Vec, + apply: impl FnOnce(&mut RegistryDoc, Vec, bool), +) { + let mut doc = lock(doc); + let (rows, withheld) = match codec { + Some(codec) => open_rows(codec.as_ref(), rows, &doc), + None => (rows, 0), + }; + let previous = doc.cursor(); + apply(&mut doc, rows, withheld == 0); + if withheld > 0 { + tracing::info!( + withheld, + "registry: rows withheld until keys arrive; cursor held" + ); + doc.hold_cursor(previous); + } +} + enum SessionEnd { /// Transport died / probe deadline / requested redial: back off, redial. Reconnect, @@ -755,14 +1004,21 @@ impl Actor { return SessionEnd::Reconnect; }; { - let mut doc = lock(&self.doc); - let outcome = doc.apply_state(seq, full, gc_floor, rows); - if full { - self.stats.full_resyncs.fetch_add(1, Relaxed); - } - if outcome == StateOutcome::Reseeded { - tracing::info!("registry: server behind local state; re-seeding"); - } + let stats = self.stats.clone(); + apply_opened( + self.codec.as_ref(), + &self.doc, + rows, + |doc, rows, complete| { + let outcome = doc.apply_state(seq, full && complete, gc_floor, rows); + if full { + stats.full_resyncs.fetch_add(1, Relaxed); + } + if outcome == StateOutcome::Reseeded { + tracing::info!("registry: server behind local state; re-seeding"); + } + }, + ); } { let now = tokio::time::Instant::now(); @@ -885,12 +1141,22 @@ impl Actor { let presence = self.presence.clone(); let stats = self.stats.clone(); let busy = self.sync_busy.clone(); + let codec = self.codec.clone(); tokio::spawn(async move { let batches: Vec = lock(&doc).take_pushable(); let mut push_failed = false; for batch in &batches { - let body = - serde_json::json!({ "batch": batch.batch, "ops": batch.ops }).to_string(); + let ops = match &codec { + Some(codec) => match seal_ops(codec.as_ref(), &batch.ops) { + Some(ops) => ops, + None => { + push_failed = true; + break; + } + }, + None => batch.ops.clone(), + }; + let body = serde_json::json!({ "batch": batch.batch, "ops": ops }).to_string(); match transport.push(body).await { Ok(ack) => { let parsed = serde_json::from_str::(&ack) @@ -945,9 +1211,9 @@ impl Actor { } match serde_json::from_str::(&body) { Ok(pull) => { - let mut d = lock(&doc); - d.apply_state(pull.seq, pull.full, pull.gc_floor, pull.rows); - drop(d); + apply_opened(codec.as_ref(), &doc, pull.rows, |d, rows, complete| { + d.apply_state(pull.seq, pull.full && complete, pull.gc_floor, rows); + }); let now = tokio::time::Instant::now(); let mut map = lock(&presence); for (device, at) in pull.presence { @@ -974,9 +1240,21 @@ impl Actor { async fn push_pending(&self, pipe: &mut TextPipe) -> bool { let batches: Vec = lock(&self.doc).take_pushable(); for batch in batches { + let ops = match &self.codec { + Some(codec) => match seal_ops(codec.as_ref(), &batch.ops) { + Some(ops) => ops, + None => { + // Keys not ready: the batch (and everything after + // it) stays pending and pushable for the next nudge. + lock(&self.doc).mark_disconnected(); + return true; + } + }, + None => batch.ops.clone(), + }; let frame = serde_json::to_string(&ClientFrame::Push { batch: &batch.batch, - ops: &batch.ops, + ops: &ops, }) .expect("push serializes"); if pipe.tx.send(frame).await.is_err() { @@ -998,7 +1276,10 @@ impl Actor { }; match frame { ServerFrame::Rows { seq, rows } => { - let contiguous = lock(&self.doc).apply_rows(seq, rows); + let mut contiguous = true; + apply_opened(self.codec.as_ref(), &self.doc, rows, |doc, rows, _| { + contiguous = doc.apply_rows(seq, rows); + }); self.stats.last_pushed_ms.store(epoch_ms(), Relaxed); let _ = self.events.send(RegistryEvent::Applied); if !contiguous { @@ -1030,9 +1311,14 @@ impl Actor { } => { // Servers only send state as a hello answer, but applying a // late duplicate is harmless and simpler than special-casing. - let mut doc = lock(&self.doc); - doc.apply_state(seq, full, gc_floor, rows); - drop(doc); + apply_opened( + self.codec.as_ref(), + &self.doc, + rows, + |doc, rows, complete| { + doc.apply_state(seq, full && complete, gc_floor, rows); + }, + ); let now = tokio::time::Instant::now(); let mut map = lock(&self.presence); for (device, at) in presence { @@ -1051,3 +1337,181 @@ impl Actor { #[cfg(any(test, feature = "mock-server"))] pub mod mock_server; + +#[cfg(test)] +mod encryption_tests { + use super::*; + + struct Codec; + + impl RegistryCodec for Codec { + fn seal_field( + &self, + _: &str, + _: &str, + _: &str, + _: &str, + value: &serde_json::Value, + ) -> Result { + Ok(value.clone()) + } + + fn open_field( + &self, + _: &str, + _: &str, + _: &str, + _: &str, + value: &serde_json::Value, + ) -> Result, FieldOpenFailure> { + match value.as_str() { + Some("bad") => Err(FieldOpenFailure::Rejected), + Some("missing") => Err(FieldOpenFailure::KeyUnavailable), + _ if value.is_null() => Ok(None), + _ => Ok(Some(value.clone())), + } + } + + fn seal_lifecycle( + &self, + kind: &str, + id: &str, + hlc: &str, + ) -> Result { + Ok(serde_json::json!({ "proof": format!("{kind}/{id}/{hlc}") })) + } + + fn open_lifecycle( + &self, + kind: &str, + id: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result<(), FieldOpenFailure> { + match wire.get("proof").and_then(|p| p.as_str()) { + Some("missing") => Err(FieldOpenFailure::KeyUnavailable), + Some(proof) if proof == format!("{kind}/{id}/{hlc}") => Ok(()), + _ => Err(FieldOpenFailure::Rejected), + } + } + } + + fn tombstone(hlc: &str, proof: Option) -> RegistryRow { + let mut row: RegistryRow = serde_json::from_value(serde_json::json!({ + "kind": "chats", "id": "chat", "seq": 3, "deleted": true, "delHlc": hlc, + })) + .unwrap(); + row.del_proof = proof; + row + } + + #[test] + fn tombstones_need_a_verified_proof_newer_than_the_baseline() { + let mut baseline = RegistryDoc::new("device"); + baseline.apply_state(1, true, 0, vec![row("trusted", "old", "2")]); + // No proof, a proof for another row, and a stale proof: all dropped. + let forged = tombstone("5", None); + let moved = tombstone("5", Some(serde_json::json!({ "proof": "chats/other/5" }))); + let stale = tombstone("1", Some(serde_json::json!({ "proof": "chats/chat/1" }))); + let (opened, withheld) = open_rows(&Codec, vec![forged, moved, stale], &baseline); + assert_eq!((opened.len(), withheld), (0, 0)); + // A missing key withholds (cursor holds); a good proof applies. + let waiting = tombstone("5", Some(serde_json::json!({ "proof": "missing" }))); + let (opened, withheld) = open_rows(&Codec, vec![waiting], &baseline); + assert_eq!((opened.len(), withheld), (0, 1)); + let good = tombstone("5", Some(serde_json::json!({ "proof": "chats/chat/5" }))); + let (opened, withheld) = open_rows(&Codec, vec![good], &baseline); + assert_eq!(withheld, 0); + assert!(opened[0].deleted && opened[0].fields.is_empty()); + assert_eq!(opened[0].del_hlc.as_deref(), Some("5")); + } + + #[test] + fn revival_over_a_verified_tombstone_needs_newer_evidence() { + let mut baseline = RegistryDoc::new("device"); + baseline.apply_state( + 1, + true, + 0, + vec![tombstone("5", Some(serde_json::json!({ "proof": "chats/chat/5" })))], + ); + // A relay replaying pre-deletion fields cannot resurrect the row. + let (opened, _) = open_rows(&Codec, vec![row("old", "old", "3")], &baseline); + assert!(opened.is_empty()); + // A member's write newer than the tombstone revives it. + let (opened, _) = open_rows(&Codec, vec![row("new", "new", "6")], &baseline); + assert_eq!(opened.len(), 1); + assert!(!opened[0].deleted); + assert_eq!(opened[0].fields["title"], "new"); + } + + fn row(title: &str, cwd: &str, clock: &str) -> RegistryRow { + serde_json::from_value(serde_json::json!({ + "kind": "chats", "id": "chat", "seq": 2, + "fields": {"title": title, "cwd": cwd}, + "clocks": {"title": clock, "cwd": clock} + })) + .unwrap() + } + + fn saved(doc: &Mutex) -> serde_json::Value { + serde_json::from_slice(&lock(doc).to_bytes().unwrap()).unwrap() + } + + #[test] + fn rejected_field_preserves_verified_value_and_clock() { + let doc = Mutex::new(RegistryDoc::new("device")); + lock(&doc).apply_state(1, true, 0, vec![row("trusted", "old", "1")]); + let codec: Arc = Arc::new(Codec); + apply_opened( + Some(&codec), + &doc, + vec![row("bad", "new", "2")], + |doc, rows, _| { + assert!(doc.apply_rows(2, rows)); + }, + ); + let state = saved(&doc); + assert_eq!(state["rows"][0]["fields"]["title"], "trusted"); + assert_eq!(state["rows"][0]["clocks"]["title"], "1"); + assert_eq!(state["rows"][0]["fields"]["cwd"], "new"); + assert_eq!(state["rows"][0]["clocks"]["cwd"], "2"); + } + + #[test] + fn authenticated_deletion_and_replay_preserve_field_clocks() { + let mut baseline = RegistryDoc::new("device"); + baseline.apply_state(1, true, 0, vec![row("trusted", "old", "2")]); + let mut incoming = row("stale", "new", "1"); + incoming + .fields + .insert("cwd".into(), serde_json::Value::Null); + incoming.clocks.insert("cwd".into(), "3".into()); + incoming.clocks.insert("injected".into(), "9".into()); + let (opened, withheld) = open_rows(&Codec, vec![incoming], &baseline); + assert_eq!(withheld, 0); + assert_eq!(opened[0].fields["title"], "trusted"); + assert_eq!(opened[0].clocks["title"], "2"); + assert!(!opened[0].fields.contains_key("cwd")); + assert_eq!(opened[0].clocks["cwd"], "3"); + assert!(!opened[0].clocks.contains_key("injected")); + } + + #[test] + fn full_sync_missing_keys_keeps_cached_rows_without_reseeding() { + let doc = Mutex::new(RegistryDoc::new("device")); + lock(&doc).apply_state(1, true, 0, vec![row("trusted", "old", "1")]); + let codec: Arc = Arc::new(Codec); + apply_opened( + Some(&codec), + &doc, + vec![row("missing", "new", "2")], + |doc, rows, complete| { + doc.apply_state(2, complete, 0, rows); + }, + ); + assert_eq!(saved(&doc)["rows"][0]["fields"]["title"], "trusted"); + assert_eq!(lock(&doc).cursor(), 1); + assert_eq!(lock(&doc).pending_len(), 0); + } +} diff --git a/crates/sync/src/store.rs b/crates/sync/src/store.rs index abd2492cb..046de2330 100644 --- a/crates/sync/src/store.rs +++ b/crates/sync/src/store.rs @@ -6,7 +6,12 @@ use std::path::Path; use std::sync::{Mutex, MutexGuard, PoisonError}; use std::time::{SystemTime, UNIX_EPOCH}; -use rusqlite::{Connection, OptionalExtension, params}; +use rusqlite::{Connection, OptionalExtension, TransactionBehavior, params}; +use zeron_crypto::content::{ContentPurpose, SealedContent}; +use zeron_crypto::record::{RecordBinding, RecordKind, UnverifiedRecord}; + +pub const MAX_ENCRYPTED_OUTBOX_BYTES: usize = 64 * 1024 * 1024; +const MAX_ENCRYPTED_RECORD_BYTES: usize = crate::chat_client::MAX_PUSH_BYTES; /// Errors surfaced by [`DocsStore`]. #[derive(Debug, thiserror::Error)] @@ -15,6 +20,20 @@ pub enum StoreError { Sqlite(#[from] rusqlite::Error), #[error("io: {0}")] Io(#[from] std::io::Error), + #[error("encrypted batch conflicts with an existing immutable record")] + EncryptedBatchConflict, + #[error("encrypted outbox capacity reached")] + EncryptedOutboxFull, + #[error("encrypted record exceeds the chat transport limit")] + EncryptedBatchTooLarge, + #[error("invalid encrypted outbox limit")] + InvalidOutboxLimit, + #[error("stored encrypted batch could not be verified")] + InvalidEncryptedBatch, + #[error("snapshot cursor exceeds the supported storage range")] + InvalidCursor, + #[error("encrypted snapshot cursor would regress")] + CursorRegression, } /// Ordered, append-only migrations. Each entry runs once inside a transaction; @@ -37,8 +56,53 @@ const MIGRATIONS: &[&str] = &[ // (M1/M3): 2 = thin chat2 rebuild; NULL/0 = pre-migration s2 doc. "ALTER TABLE snapshots ADD COLUMN cursor INTEGER; ALTER TABLE snapshots ADD COLUMN epoch INTEGER;", + "CREATE TABLE encrypted_outbox ( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + batch_id BLOB NOT NULL UNIQUE CHECK(length(batch_id) = 16), + doc_id TEXT NOT NULL CHECK(length(doc_id) BETWEEN 1 AND 256), + vault_id BLOB NOT NULL CHECK(length(vault_id) = 16), + generation BLOB NOT NULL CHECK(length(generation) = 16), + key_epoch BLOB NOT NULL CHECK(length(key_epoch) = 8), + object_id BLOB NOT NULL CHECK(length(object_id) = 16), + author_id BLOB NOT NULL CHECK(length(author_id) = 16), + membership_hash BLOB NOT NULL CHECK(length(membership_hash) = 32), + record BLOB NOT NULL CHECK(length(record) BETWEEN 1 AND 1044480), + queued_at INTEGER NOT NULL + ) STRICT; + CREATE INDEX encrypted_outbox_scope ON encrypted_outbox + (vault_id, generation, key_epoch, object_id, author_id, membership_hash, sequence);", ]; +#[derive(Clone)] +pub struct PendingEncryptedBatch { + sequence: i64, + doc_id: String, + binding: RecordBinding, + revision_id: [u8; 16], + encoded: Vec, +} + +impl PendingEncryptedBatch { + pub fn doc_id(&self) -> &str { + &self.doc_id + } + pub fn binding(&self) -> &RecordBinding { + &self.binding + } + pub fn revision_id(&self) -> &[u8; 16] { + &self.revision_id + } + pub fn encoded(&self) -> &[u8] { + &self.encoded + } +} + +impl std::fmt::Debug for PendingEncryptedBatch { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("PendingEncryptedBatch([REDACTED])") + } +} + /// SQLite-backed store under a data directory (`{data_dir}/docs.sqlite3`). /// /// Holds warm-open doc snapshots (the DO room is authoritative; these make @@ -198,6 +262,282 @@ impl DocsStore { Ok(changed > 0) } + pub fn persist_encrypted_batch( + &self, + doc_id: &str, + snapshot: &[u8], + cursor: u64, + document_epoch: u32, + sealed: &SealedContent, + max_pending_bytes: usize, + ) -> Result { + if max_pending_bytes > MAX_ENCRYPTED_OUTBOX_BYTES { + return Err(StoreError::InvalidOutboxLimit); + } + if doc_id.is_empty() || doc_id.len() > 256 || sealed.purpose() != ContentPurpose::ChatUpdate + { + return Err(StoreError::InvalidEncryptedBatch); + } + if sealed.encoded().len() > MAX_ENCRYPTED_RECORD_BYTES { + return Err(StoreError::EncryptedBatchTooLarge); + } + let cursor = i64::try_from(cursor).map_err(|_| StoreError::InvalidCursor)?; + let binding = sealed.binding(); + let mut connection = self.conn(); + connection.pragma_update(None, "synchronous", "FULL")?; + let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?; + let existing: Option<(i64, String, Vec)> = transaction + .query_row( + "SELECT sequence, doc_id, record FROM encrypted_outbox WHERE batch_id = ?1", + params![sealed.revision_id().as_slice()], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + ) + .optional()?; + if let Some((sequence, existing_doc_id, encoded)) = existing { + if existing_doc_id != doc_id || encoded != sealed.encoded() { + return Err(StoreError::EncryptedBatchConflict); + } + transaction.commit()?; + return Ok(PendingEncryptedBatch { + sequence, + doc_id: existing_doc_id, + binding: *binding, + revision_id: *sealed.revision_id(), + encoded, + }); + } + let stored_cursor: Option> = transaction + .query_row( + "SELECT cursor FROM snapshots WHERE doc_id = ?1", + params![doc_id], + |row| row.get(0), + ) + .optional()?; + if stored_cursor + .flatten() + .is_some_and(|stored| stored < 0 || cursor < stored) + { + return Err(StoreError::CursorRegression); + } + let queued_bytes: i64 = transaction.query_row( + "SELECT COALESCE(SUM(length(record)), 0) FROM encrypted_outbox", + [], + |row| row.get(0), + )?; + let queued_bytes = + usize::try_from(queued_bytes).map_err(|_| StoreError::InvalidEncryptedBatch)?; + if queued_bytes + .checked_add(sealed.encoded().len()) + .is_none_or(|total| total > max_pending_bytes) + { + return Err(StoreError::EncryptedOutboxFull); + } + transaction.execute( + "INSERT INTO snapshots (doc_id, bytes, saved_at, cursor, epoch) VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(doc_id) DO UPDATE SET bytes = excluded.bytes, saved_at = excluded.saved_at, + cursor = excluded.cursor, epoch = excluded.epoch", + params![doc_id, snapshot, now_ms(), cursor, i64::from(document_epoch)], + )?; + transaction.execute( + "INSERT INTO encrypted_outbox + (batch_id, doc_id, vault_id, generation, key_epoch, object_id, author_id, membership_hash, record, queued_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![sealed.revision_id().as_slice(), doc_id, binding.vault_id.as_slice(), binding.generation.as_slice(), + binding.epoch.to_be_bytes().as_slice(), binding.object_id.as_slice(), binding.author_id.as_slice(), + binding.membership_hash.as_slice(), sealed.encoded(), now_ms()], + )?; + let sequence = transaction.last_insert_rowid(); + transaction.commit()?; + Ok(PendingEncryptedBatch { + sequence, + doc_id: doc_id.to_owned(), + binding: *binding, + revision_id: *sealed.revision_id(), + encoded: sealed.encoded().to_vec(), + }) + } + + pub fn pending_encrypted_batches( + &self, + binding: &RecordBinding, + trusted_public_key: &[u8], + max_batches: usize, + ) -> Result, StoreError> { + if max_batches > 128 { + return Err(StoreError::InvalidOutboxLimit); + } + if binding.kind != RecordKind::Content { + return Err(StoreError::InvalidEncryptedBatch); + } + let connection = self.conn(); + let mut statement = connection.prepare( + "SELECT sequence, doc_id, batch_id, length(record), record FROM encrypted_outbox + WHERE vault_id = ?1 AND generation = ?2 AND key_epoch = ?3 AND object_id = ?4 + AND author_id = ?5 AND membership_hash = ?6 ORDER BY sequence LIMIT ?7", + )?; + let mut rows = statement.query(params![ + binding.vault_id.as_slice(), + binding.generation.as_slice(), + binding.epoch.to_be_bytes().as_slice(), + binding.object_id.as_slice(), + binding.author_id.as_slice(), + binding.membership_hash.as_slice(), + max_batches as i64 + ])?; + let mut pending = Vec::new(); + let mut loaded_bytes = 0usize; + while let Some(row) = rows.next()? { + let length = usize::try_from(row.get::<_, i64>(3)?) + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + loaded_bytes = loaded_bytes + .checked_add(length) + .ok_or(StoreError::InvalidEncryptedBatch)?; + if length > MAX_ENCRYPTED_RECORD_BYTES || loaded_bytes > MAX_ENCRYPTED_OUTBOX_BYTES { + return Err(StoreError::InvalidEncryptedBatch); + } + let sequence = row.get(0)?; + let doc_id: String = row.get(1)?; + let revision_bytes: Vec = row.get(2)?; + let revision_id: [u8; 16] = revision_bytes + .as_slice() + .try_into() + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + let encoded: Vec = row.get(4)?; + if doc_id.is_empty() || doc_id.len() > 256 || encoded.len() != length { + return Err(StoreError::InvalidEncryptedBatch); + } + let parsed = UnverifiedRecord::parse(&encoded, MAX_ENCRYPTED_RECORD_BYTES) + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + if parsed.untrusted_revision_id() != &revision_id { + return Err(StoreError::InvalidEncryptedBatch); + } + parsed + .verify(binding, trusted_public_key) + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + pending.push(PendingEncryptedBatch { + sequence, + doc_id, + binding: *binding, + revision_id, + encoded, + }); + } + Ok(pending) + } + + /// Every queued batch for `doc_id` authored by this device, in FIFO + /// order, regardless of the epoch/policy it was sealed under — the + /// reconnect path re-seals stale ones under the current policy (RFC 0001 + /// §11) instead of stranding them. Each record is re-verified against + /// its stored binding with the caller's own public key. + pub fn pending_encrypted_batches_for_doc( + &self, + doc_id: &str, + author_id: &[u8; 16], + author_public_key: &[u8], + max_batches: usize, + ) -> Result, StoreError> { + if max_batches > 128 { + return Err(StoreError::InvalidOutboxLimit); + } + let connection = self.conn(); + let mut statement = connection.prepare( + "SELECT sequence, batch_id, length(record), record, vault_id, generation, key_epoch, + object_id, membership_hash + FROM encrypted_outbox WHERE doc_id = ?1 AND author_id = ?2 ORDER BY sequence LIMIT ?3", + )?; + let mut rows = + statement.query(params![doc_id, author_id.as_slice(), max_batches as i64])?; + let mut pending = Vec::new(); + let mut loaded_bytes = 0usize; + while let Some(row) = rows.next()? { + let length = usize::try_from(row.get::<_, i64>(2)?) + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + loaded_bytes = loaded_bytes + .checked_add(length) + .ok_or(StoreError::InvalidEncryptedBatch)?; + if length > MAX_ENCRYPTED_RECORD_BYTES || loaded_bytes > MAX_ENCRYPTED_OUTBOX_BYTES { + return Err(StoreError::InvalidEncryptedBatch); + } + let fixed16 = |bytes: Vec| -> Result<[u8; 16], StoreError> { + bytes + .as_slice() + .try_into() + .map_err(|_| StoreError::InvalidEncryptedBatch) + }; + let sequence = row.get(0)?; + let revision_id = fixed16(row.get(1)?)?; + let encoded: Vec = row.get(3)?; + let key_epoch: Vec = row.get(6)?; + let membership: Vec = row.get(8)?; + let binding = RecordBinding { + kind: RecordKind::Content, + vault_id: fixed16(row.get(4)?)?, + generation: fixed16(row.get(5)?)?, + epoch: u64::from_be_bytes( + key_epoch + .as_slice() + .try_into() + .map_err(|_| StoreError::InvalidEncryptedBatch)?, + ), + object_id: fixed16(row.get(7)?)?, + author_id: *author_id, + membership_hash: membership + .as_slice() + .try_into() + .map_err(|_| StoreError::InvalidEncryptedBatch)?, + }; + if encoded.len() != length { + return Err(StoreError::InvalidEncryptedBatch); + } + let parsed = UnverifiedRecord::parse(&encoded, MAX_ENCRYPTED_RECORD_BYTES) + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + if parsed.untrusted_revision_id() != &revision_id { + return Err(StoreError::InvalidEncryptedBatch); + } + parsed + .verify(&binding, author_public_key) + .map_err(|_| StoreError::InvalidEncryptedBatch)?; + pending.push(PendingEncryptedBatch { + sequence, + doc_id: doc_id.to_owned(), + binding, + revision_id, + encoded, + }); + } + Ok(pending) + } + + pub fn acknowledge_encrypted_batch( + &self, + batch: &PendingEncryptedBatch, + ) -> Result { + let connection = self.conn(); + connection.pragma_update(None, "synchronous", "FULL")?; + let removed = connection.execute( + "DELETE FROM encrypted_outbox WHERE sequence = ?1 AND batch_id = ?2 AND record = ?3", + params![ + batch.sequence, + batch.revision_id.as_slice(), + batch.encoded.as_slice() + ], + )?; + Ok(removed == 1) + } + + pub fn encrypted_outbox_stats(&self) -> Result<(usize, usize), StoreError> { + let (count, bytes): (i64, i64) = self.conn().query_row( + "SELECT COUNT(*), COALESCE(SUM(length(record)), 0) FROM encrypted_outbox", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + )?; + Ok(( + usize::try_from(count).map_err(|_| StoreError::InvalidEncryptedBatch)?, + usize::try_from(bytes).map_err(|_| StoreError::InvalidEncryptedBatch)?, + )) + } + fn conn(&self) -> MutexGuard<'_, Connection> { // A poisoned lock only means another thread panicked mid-query; the // connection itself is still usable. diff --git a/crates/sync/tests/encrypted_outbox.rs b/crates/sync/tests/encrypted_outbox.rs new file mode 100644 index 000000000..7ca842769 --- /dev/null +++ b/crates/sync/tests/encrypted_outbox.rs @@ -0,0 +1,352 @@ +use zeron_crypto::content::{ + self, ContentKey, ContentPurpose, DeviceSigner, KeyScope, SealedContent, +}; +use zeron_crypto::record::{RecordBinding, RecordKind}; +use zeron_sync::{DocsStore, MAX_ENCRYPTED_OUTBOX_BYTES, StoreError}; + +type TestResult = Result<(), Box>; + +fn binding(epoch: u64) -> RecordBinding { + RecordBinding { + kind: RecordKind::Content, + vault_id: [1; 16], + generation: [2; 16], + epoch, + object_id: [3; 16], + author_id: [4; 16], + membership_hash: [5; 32], + } +} + +fn seal( + epoch: u64, + plaintext: &[u8], +) -> Result<(SealedContent, ContentKey, DeviceSigner), content::ContentError> { + let context = binding(epoch); + let key = ContentKey::generate(KeyScope::from(&context))?; + let signer = DeviceSigner::from_seed([4; 16], &[8; 32])?; + let sealed = content::seal( + &context, + ContentPurpose::ChatUpdate, + &key, + &signer, + plaintext, + 1024, + )?; + Ok((sealed, key, signer)) +} + +#[test] +fn non_chat_and_oversized_records_cannot_enter_the_chat_outbox() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + let (_, key, signer) = seal(1, b"command")?; + let blob = content::seal(&binding(1), ContentPurpose::Blob, &key, &signer, b"blob", 4)?; + assert!(matches!( + store.persist_encrypted_batch( + "chat-1", + b"snapshot", + 0, + 2, + &blob, + MAX_ENCRYPTED_OUTBOX_BYTES + ), + Err(StoreError::InvalidEncryptedBatch) + )); + let plaintext = vec![42; zeron_sync::chat_client::MAX_PUSH_BYTES]; + let oversized = content::seal( + &binding(1), + ContentPurpose::ChatUpdate, + &key, + &signer, + &plaintext, + plaintext.len(), + )?; + assert!(matches!( + store.persist_encrypted_batch( + "chat-1", + b"snapshot", + 0, + 2, + &oversized, + MAX_ENCRYPTED_OUTBOX_BYTES + ), + Err(StoreError::EncryptedBatchTooLarge) + )); + assert_eq!(store.encrypted_outbox_stats()?.0, 0); + assert!(!store.has_snapshot("chat-1")?); + Ok(()) +} + +#[test] +fn full_width_key_epochs_round_trip_without_sqlite_integer_truncation() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + let (sealed, _, signer) = seal(u64::MAX, b"command")?; + store.persist_encrypted_batch( + "chat-1", + b"snapshot", + 0, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES, + )?; + let pending = store.pending_encrypted_batches(&binding(u64::MAX), signer.public_key(), 16)?; + assert_eq!( + pending + .first() + .ok_or("missing full-width epoch batch")? + .binding() + .epoch, + u64::MAX + ); + Ok(()) +} + +#[test] +fn snapshot_and_ciphertext_survive_restart_and_ack() -> TestResult { + let directory = tempfile::tempdir()?; + let (sealed, key, signer) = seal(1, b"private command canary")?; + { + let store = DocsStore::open(directory.path())?; + let receipt = store.persist_encrypted_batch( + "chat-1", + b"snapshot", + 7, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES, + )?; + assert_eq!(receipt.encoded(), sealed.encoded()); + assert_eq!(receipt.revision_id(), sealed.revision_id()); + assert_eq!( + store.load_snapshot_with_cursor("chat-1")?, + Some((b"snapshot".to_vec(), 7, 2)) + ); + } + let store = DocsStore::open(directory.path())?; + let pending = store.pending_encrypted_batches(&binding(1), signer.public_key(), 16)?; + assert_eq!(pending.len(), 1); + let receipt = pending.first().ok_or("missing persisted batch")?; + assert_eq!(receipt.encoded(), sealed.encoded()); + let plaintext = content::open( + receipt.encoded(), + &binding(1), + ContentPurpose::ChatUpdate, + &key, + signer.public_key(), + 1024, + )?; + assert_eq!(plaintext.plaintext().as_bytes(), b"private command canary"); + assert!(store.acknowledge_encrypted_batch(receipt)?); + assert!(!store.acknowledge_encrypted_batch(receipt)?); + assert_eq!(store.encrypted_outbox_stats()?.0, 0); + assert!(store.has_snapshot("chat-1")?); + Ok(()) +} + +#[test] +fn duplicate_enqueue_does_not_rewrite_snapshot_or_ciphertext() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + let (sealed, _, signer) = seal(1, b"command")?; + store.persist_encrypted_batch( + "chat-1", + b"new snapshot", + 9, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES, + )?; + store.persist_encrypted_batch( + "chat-1", + b"stale retry", + 0, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES, + )?; + assert_eq!( + store.load_snapshot_with_cursor("chat-1")?, + Some((b"new snapshot".to_vec(), 9, 2)) + ); + assert_eq!( + store + .pending_encrypted_batches(&binding(1), signer.public_key(), 16)? + .len(), + 1 + ); + assert!(matches!( + store.persist_encrypted_batch( + "another-doc", + b"wrong", + 0, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES + ), + Err(StoreError::EncryptedBatchConflict) + )); + assert!(!store.has_snapshot("another-doc")?); + Ok(()) +} + +#[test] +fn outbox_failure_rolls_back_the_snapshot() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + store.save_snapshot_with_cursor("chat-1", b"old snapshot", 3, 2)?; + let connection = rusqlite::Connection::open(directory.path().join("docs.sqlite3"))?; + connection.execute_batch("CREATE TRIGGER fail_outbox BEFORE INSERT ON encrypted_outbox BEGIN SELECT RAISE(ABORT, 'test failure'); END;")?; + let (sealed, _, _) = seal(1, b"command")?; + assert!( + store + .persist_encrypted_batch( + "chat-1", + b"new snapshot", + 4, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES + ) + .is_err() + ); + assert_eq!( + store.load_snapshot_with_cursor("chat-1")?, + Some((b"old snapshot".to_vec(), 3, 2)) + ); + assert_eq!(store.encrypted_outbox_stats()?.0, 0); + Ok(()) +} + +#[test] +fn quotas_and_cursor_regression_preserve_last_good_state() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + store.save_snapshot_with_cursor("chat-1", b"old", 5, 2)?; + let (sealed, _, _) = seal(1, b"command")?; + assert!(matches!( + store.persist_encrypted_batch("chat-1", b"new", 5, 2, &sealed, 0), + Err(StoreError::EncryptedOutboxFull) + )); + assert!(matches!( + store.persist_encrypted_batch( + "chat-1", + b"old cursor", + 4, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES + ), + Err(StoreError::CursorRegression) + )); + assert!(matches!( + store.persist_encrypted_batch( + "chat-1", + b"overflow", + u64::MAX, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES + ), + Err(StoreError::InvalidCursor) + )); + assert_eq!( + store.load_snapshot_with_cursor("chat-1")?, + Some((b"old".to_vec(), 5, 2)) + ); + assert_eq!(store.encrypted_outbox_stats()?.0, 0); + Ok(()) +} + +#[test] +fn replay_filters_policy_and_keeps_old_epoch_work() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + let (first, _, signer) = seal(1, b"first")?; + let (second, _, _) = seal(1, b"second")?; + store.persist_encrypted_batch("chat-1", b"one", 0, 2, &first, MAX_ENCRYPTED_OUTBOX_BYTES)?; + store.persist_encrypted_batch("chat-1", b"two", 0, 2, &second, MAX_ENCRYPTED_OUTBOX_BYTES)?; + let pending = store.pending_encrypted_batches(&binding(1), signer.public_key(), 1)?; + assert_eq!( + pending.first().ok_or("missing first batch")?.revision_id(), + first.revision_id() + ); + assert!( + store + .pending_encrypted_batches(&binding(2), signer.public_key(), 16)? + .is_empty() + ); + let mut changed_policy = binding(1); + changed_policy.membership_hash = [9; 32]; + assert!( + store + .pending_encrypted_batches(&changed_policy, signer.public_key(), 16)? + .is_empty() + ); + assert_eq!(store.encrypted_outbox_stats()?.0, 2); + Ok(()) +} + +#[test] +fn concurrent_connections_cannot_overfill_the_outbox() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + let first_store = DocsStore::open(directory.path())?; + let second_store = DocsStore::open(directory.path())?; + let (first, _, _) = seal(1, b"a")?; + let (second, _, _) = seal(1, b"b")?; + assert_eq!(first.encoded().len(), second.encoded().len()); + let quota = first.encoded().len(); + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let mut writers = Vec::new(); + for (connection, sealed) in [(first_store, first), (second_store, second)] { + let barrier = barrier.clone(); + writers.push(std::thread::spawn(move || { + barrier.wait(); + match connection.persist_encrypted_batch("chat-1", b"snapshot", 0, 2, &sealed, quota) { + Ok(_) => Ok(true), + Err(StoreError::EncryptedOutboxFull) => Ok(false), + Err(error) => Err(error), + } + })); + } + let mut committed = 0; + for writer in writers { + if writer.join().map_err(|_| "outbox writer panicked")?? { + committed += 1; + } + } + assert_eq!(committed, 1); + assert_eq!(store.encrypted_outbox_stats()?, (1, quota)); + Ok(()) +} + +#[test] +fn corrupted_stored_ciphertext_is_not_replayed_or_deleted() -> TestResult { + let directory = tempfile::tempdir()?; + let store = DocsStore::open(directory.path())?; + let (sealed, _, signer) = seal(1, b"command")?; + let receipt = store.persist_encrypted_batch( + "chat-1", + b"snapshot", + 0, + 2, + &sealed, + MAX_ENCRYPTED_OUTBOX_BYTES, + )?; + let mut damaged = sealed.encoded().to_vec(); + *damaged.last_mut().ok_or("empty encrypted record")? ^= 1; + let connection = rusqlite::Connection::open(directory.path().join("docs.sqlite3"))?; + connection.execute( + "UPDATE encrypted_outbox SET record = ?1 WHERE batch_id = ?2", + rusqlite::params![damaged, sealed.revision_id().as_slice()], + )?; + assert!(matches!( + store.pending_encrypted_batches(&binding(1), signer.public_key(), 16), + Err(StoreError::InvalidEncryptedBatch) + )); + assert!(!store.acknowledge_encrypted_batch(&receipt)?); + assert_eq!(store.encrypted_outbox_stats()?.0, 1); + Ok(()) +} diff --git a/crates/sync/tests/registry_client.rs b/crates/sync/tests/registry_client.rs index bfc1ec74b..f8aa3417b 100644 --- a/crates/sync/tests/registry_client.rs +++ b/crates/sync/tests/registry_client.rs @@ -18,6 +18,7 @@ fn ts(ms: i64) -> DateTime { fn device(id: &str) -> Device { Device { + vault_device_id: None, id: id.into(), name: format!("{id}-name"), platform: "linux".into(), @@ -565,3 +566,330 @@ async fn unreadable_http_ack_retries_instead_of_stranding_the_batch() { client.shutdown().await; } + +// ── encrypted profile: per-field codec (RFC 0001 §9) ──────────────────────── + +/// A stand-in codec: values travel as `{"sealed": }` +/// so the test can observe (a) the server only ever holds the wire form, +/// (b) a value moved between fields/rows/clocks is rejected, (c) a device +/// without keys withholds rows and holds its cursor until keys arrive. +struct TestCodec { + has_keys: std::sync::atomic::AtomicBool, +} + +impl TestCodec { + fn new(has_keys: bool) -> Self { + Self { + has_keys: std::sync::atomic::AtomicBool::new(has_keys), + } + } +} + +impl zeron_sync::RegistryCodec for TestCodec { + fn seal_field( + &self, + kind: &str, + id: &str, + field: &str, + hlc: &str, + value: &serde_json::Value, + ) -> Result { + if !self.has_keys.load(std::sync::atomic::Ordering::Relaxed) { + return Err("no keys".into()); + } + let inner = serde_json::json!({ "kind": kind, "id": id, "field": field, "hlc": hlc, "value": value }) + .to_string(); + // Obscure the plaintext the way a real seal would (the assertions + // scan the relay's copy for canaries): reversed hex is enough here. + let wire: String = inner.bytes().rev().map(|b| format!("{b:02x}")).collect(); + Ok(serde_json::json!({ "sealed": wire })) + } + + fn open_field( + &self, + kind: &str, + id: &str, + field: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result, zeron_sync::FieldOpenFailure> { + if !self.has_keys.load(std::sync::atomic::Ordering::Relaxed) { + return Err(zeron_sync::FieldOpenFailure::KeyUnavailable); + } + let Some(text) = wire.get("sealed").and_then(|v| v.as_str()) else { + return Err(zeron_sync::FieldOpenFailure::Rejected); // plaintext where ciphertext is required + }; + let bytes: Vec = (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).unwrap()) + .rev() + .collect(); + let inner: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + if inner["kind"] != kind + || inner["id"] != id + || inner["field"] != field + || inner["hlc"] != hlc + { + return Err(zeron_sync::FieldOpenFailure::Rejected); + } + Ok(if inner["value"].is_null() { + None + } else { + Some(inner["value"].clone()) + }) + } + + fn seal_lifecycle(&self, kind: &str, id: &str, hlc: &str) -> Result { + if !self.has_keys.load(std::sync::atomic::Ordering::Relaxed) { + return Err("no keys".into()); + } + let inner = + serde_json::json!({ "kind": kind, "id": id, "op": "delete", "hlc": hlc }).to_string(); + let wire: String = inner.bytes().rev().map(|b| format!("{b:02x}")).collect(); + Ok(serde_json::json!({ "sealed": wire })) + } + + fn open_lifecycle( + &self, + kind: &str, + id: &str, + hlc: &str, + wire: &serde_json::Value, + ) -> Result<(), zeron_sync::FieldOpenFailure> { + if !self.has_keys.load(std::sync::atomic::Ordering::Relaxed) { + return Err(zeron_sync::FieldOpenFailure::KeyUnavailable); + } + let Some(text) = wire.get("sealed").and_then(|v| v.as_str()) else { + return Err(zeron_sync::FieldOpenFailure::Rejected); + }; + let bytes: Vec = (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).unwrap()) + .rev() + .collect(); + let inner: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + if inner["kind"] != kind + || inner["id"] != id + || inner["op"] != "delete" + || inner["hlc"] != hlc + { + return Err(zeron_sync::FieldOpenFailure::Rejected); + } + Ok(()) + } +} + +#[tokio::test] +async fn codec_seals_every_value_and_opens_it_on_the_peer() { + let server = MockRegistryServer::start().await; + let doc_a = new_doc("dev-a"); + let doc_b = new_doc("dev-b"); + { + let mut doc = doc_a.lock().unwrap(); + doc.upsert_chat(&chat("chat-1", "dev-a")).unwrap(); + } + let client_a = RegistryClient::connect_via_codec( + Arc::new(zeron_sync::StaticUrl(server.url())), + doc_a.clone(), + "dev-a", + Arc::new(TestCodec::new(true)), + ) + .await + .expect("A joins"); + wait_until(|| server.row("chats", "chat-1").is_some()).await; + let stored = server.row("chats", "chat-1").unwrap(); + // The relay holds only the wire form: no plaintext title/cwd anywhere. + let dump = serde_json::to_string(&stored).unwrap(); + assert!(!dump.contains("\"chat\""), "title leaked: {dump}"); + assert!(!dump.contains("/tmp"), "cwd leaked: {dump}"); + assert!(stored.fields.values().all(|v| v.get("sealed").is_some())); + // The clocks are still the server's merge input (per-field LWW intact). + assert!(stored.clocks.contains_key("title")); + + let client_b = RegistryClient::connect_via_codec( + Arc::new(zeron_sync::StaticUrl(server.url())), + doc_b.clone(), + "dev-b", + Arc::new(TestCodec::new(true)), + ) + .await + .expect("B joins"); + wait_until(|| { + doc_b + .lock() + .unwrap() + .read_chats() + .map(|c| { + c.iter() + .any(|c| c.id == "chat-1" && c.title.as_deref() == Some("chat")) + }) + .unwrap_or(false) + }) + .await; + // B edits one field; A sees only that field change (merge granularity). + { + let mut doc = doc_b.lock().unwrap(); + doc.rename_chat("chat-1", "renamed").unwrap(); + } + client_b.nudge(); + wait_until(|| { + doc_a + .lock() + .unwrap() + .read_chats() + .map(|c| { + c.iter() + .any(|c| c.id == "chat-1" && c.title.as_deref() == Some("renamed")) + }) + .unwrap_or(false) + }) + .await; + assert_eq!( + doc_a.lock().unwrap().read_chats().unwrap()[0] + .cwd + .as_deref(), + Some("/tmp") + ); + client_a.shutdown().await; + client_b.shutdown().await; +} + +/// Row lifecycle proofs (RFC 0001 §9): a member's delete carries a sealed +/// proof and tombstones the row on every peer; a tombstone the relay makes +/// up (no proof) is dropped and the verified row survives. +#[tokio::test] +async fn authenticated_tombstones_delete_on_peers_and_forged_ones_do_not() { + let server = MockRegistryServer::start().await; + let doc_a = new_doc("dev-a"); + let doc_b = new_doc("dev-b"); + { + let mut doc = doc_a.lock().unwrap(); + doc.upsert_chat(&chat("chat-1", "dev-a")).unwrap(); + doc.upsert_chat(&chat("chat-2", "dev-a")).unwrap(); + } + let client_a = RegistryClient::connect_via_codec( + Arc::new(zeron_sync::StaticUrl(server.url())), + doc_a.clone(), + "dev-a", + Arc::new(TestCodec::new(true)), + ) + .await + .expect("A joins"); + let client_b = RegistryClient::connect_via_codec( + Arc::new(zeron_sync::StaticUrl(server.url())), + doc_b.clone(), + "dev-b", + Arc::new(TestCodec::new(true)), + ) + .await + .expect("B joins"); + let b_has = |id: &'static str| { + let doc_b = doc_b.clone(); + move || { + doc_b + .lock() + .unwrap() + .read_chats() + .map(|c| c.iter().any(|c| c.id == id)) + .unwrap_or(false) + } + }; + wait_until(b_has("chat-2")).await; + wait_until(b_has("chat-1")).await; + + // A deletes chat-1: the tombstone on the relay carries the sealed proof + // and B's row goes away. + { + let mut doc = doc_a.lock().unwrap(); + doc.delete_chat("chat-1").unwrap(); + } + client_a.nudge(); + wait_until(|| server.row("chats", "chat-1").is_some_and(|r| r.deleted)).await; + let stored = server.row("chats", "chat-1").unwrap(); + assert!( + stored + .del_proof + .as_ref() + .is_some_and(|p| p.get("sealed").is_some()), + "tombstone carries the sealed proof: {stored:?}" + ); + wait_until(|| !b_has("chat-1")()).await; + + // A forger with no keys (a compromised relay, in effect) tombstones + // chat-2 with a bare delete: the server merges it, B keeps its row. + let doc_f = new_doc("dev-f"); + { + let mut doc = doc_f.lock().unwrap(); + doc.upsert_chat(&chat("chat-2", "dev-f")).unwrap(); + doc.delete_chat("chat-2").unwrap(); + } + let client_f = RegistryClient::connect(&server.url(), doc_f.clone(), "dev-f") + .await + .expect("forger joins"); + wait_until(|| server.row("chats", "chat-2").is_some_and(|r| r.deleted)).await; + assert!(server.row("chats", "chat-2").unwrap().del_proof.is_none()); + tokio::time::sleep(Duration::from_millis(300)).await; + assert!( + b_has("chat-2")(), + "a tombstone without a proof must not delete B's row" + ); + assert!(!b_has("chat-1")(), "the proven tombstone stays applied"); + + client_a.shutdown().await; + client_b.shutdown().await; + client_f.shutdown().await; +} + +#[tokio::test] +async fn rows_without_keys_are_withheld_and_the_cursor_holds() { + let server = MockRegistryServer::start().await; + let doc_a = new_doc("dev-a"); + { + let mut doc = doc_a.lock().unwrap(); + doc.upsert_chat(&chat("chat-1", "dev-a")).unwrap(); + } + let client_a = RegistryClient::connect_via_codec( + Arc::new(zeron_sync::StaticUrl(server.url())), + doc_a.clone(), + "dev-a", + Arc::new(TestCodec::new(true)), + ) + .await + .expect("A joins"); + wait_until(|| server.row("chats", "chat-1").is_some()).await; + + // A device with no keys yet: nothing materializes, cursor stays at 0. + let doc_b = new_doc("dev-b"); + let codec_b = Arc::new(TestCodec::new(false)); + let client_b = RegistryClient::connect_via_codec( + Arc::new(zeron_sync::StaticUrl(server.url())), + doc_b.clone(), + "dev-b", + codec_b.clone(), + ) + .await + .expect("B joins"); + tokio::time::sleep(Duration::from_millis(200)).await; + { + let doc = doc_b.lock().unwrap(); + assert!(doc.read_chats().unwrap().is_empty(), "no row without keys"); + assert_eq!(doc.cursor(), 0, "cursor held over withheld rows"); + } + // Keys arrive: a redial re-pulls from the held cursor and applies. + codec_b + .has_keys + .store(true, std::sync::atomic::Ordering::Relaxed); + client_b.redial(); + wait_until(|| { + doc_b + .lock() + .unwrap() + .read_chats() + .map(|c| c.iter().any(|c| c.id == "chat-1")) + .unwrap_or(false) + }) + .await; + assert!(doc_b.lock().unwrap().cursor() >= 1); + client_a.shutdown().await; + client_b.shutdown().await; +} diff --git a/crates/ui/src/notify.rs b/crates/ui/src/notify.rs index 39523fe1a..ba38d4cdf 100644 --- a/crates/ui/src/notify.rs +++ b/crates/ui/src/notify.rs @@ -270,3 +270,38 @@ mod tests { assert_eq!(applescript_escape("two\nlines\r\n"), "two lines "); } } + +/// Remember request IDs across polls, including temporary disappearance from +/// the relay. A reapproval uses a new request ID and must notify again. +#[derive(Default)] +pub(crate) struct ApprovalRequests(std::collections::HashSet); + +impl ApprovalRequests { + pub(crate) fn observe<'a>( + &mut self, + vault: &str, + ids: impl IntoIterator, + ) -> bool { + let mut fresh = false; + for id in ids { + fresh |= self.0.insert(format!("{vault}/{id}")); + } + fresh + } +} + +#[cfg(test)] +mod approval_tests { + use super::ApprovalRequests; + #[test] + fn approval_notifications_deduplicate_but_reapproval_notifies() { + let mut seen = ApprovalRequests::default(); + assert!(seen.observe("vault", ["first"])); + assert!(!seen.observe("vault", ["first"])); + assert!(!seen.observe("vault", [])); + assert!(!seen.observe("vault", ["first"])); + assert!(seen.observe("vault", ["reapproval"])); + assert!(!seen.observe("vault", ["reapproval"])); + assert!(seen.observe("another-vault", ["first"])); + } +} diff --git a/crates/ui/src/settings.rs b/crates/ui/src/settings.rs index 0b676c875..44dc60c07 100644 --- a/crates/ui/src/settings.rs +++ b/crates/ui/src/settings.rs @@ -20,6 +20,7 @@ pub mod archived; pub mod composer; pub mod devices; pub mod files; +pub mod encryption; pub mod harnesses; pub mod notifications; pub mod shortcuts; diff --git a/crates/ui/src/settings/encryption.rs b/crates/ui/src/settings/encryption.rs new file mode 100644 index 000000000..314c17be7 --- /dev/null +++ b/crates/ui/src/settings/encryption.rs @@ -0,0 +1,1039 @@ +//! Settings → Encryption (RFC 0001 §4): the per-profile vault's explicit +//! states and the four actions a person can take — set up a vault (and save +//! the recovery kit), approve this device from another one, approve other +//! devices by comparing their code, and remove a device (rotating keys). +//! +//! Everything here is a thin view over the engine's `Vault*` RPCs; no key +//! material ever reaches the UI except the recovery kit text, which is shown +//! once for the user to save and never persisted by the UI. + +use gpui::{ + AnyElement, ClipboardItem, Context, Entity, IntoElement, Render, SharedString, Subscription, + Task, Window, div, prelude::*, px, +}; +use serde_json::Value; + +use zeron_proto::WorkspaceScope; +use zeron_rpc::methods; + +use crate::composer::{ComposerInput, ComposerInputEvent}; +use crate::popover::{self, Loadable}; +use crate::settings::widgets; +use crate::state::AppState; +use crate::theme::Theme; + +/// Which text prompt is open (the page has one input at a time). +enum Prompt { + /// Recovery: the user types the kit text. + Recover, + Rename { + device_id: String, + }, +} + +struct PromptDialog { + kind: Prompt, + input: Entity, + _events: Subscription, +} + +pub struct EncryptionPage { + state: Entity, + status: Loadable, + pending: Vec, + /// Shown once after setup: (kit text, recovery file JSON). + kit: Option<(String, String)>, + kit_copied: bool, + prompt: Option, + error: Option, + busy: bool, + load_task: Option>, + action_task: Option>, + copy_task: Option>, + poll_task: Option>, + _observe: Subscription, +} + +/// Human copy for each vault phase (RFC §4.3 state table). +pub fn phase_copy(status: &Value) -> (&'static str, String) { + let phase = status.get("phase").and_then(Value::as_str).unwrap_or(""); + let reason = status + .get("reason") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + match phase { + "ready" => ("Vault ready", "Synced content is encrypted on your approved devices.".into()), + "notEnrolled" => { + if status.get("remoteVault").and_then(Value::as_bool) == Some(true) { + ("Approve this device", "This account already has an encrypted vault. Approve this device from another device, or use your recovery key.".into()) + } else { + ("Not set up", "Sessions, files and workspace details are sent to the sync backend in the clear until you set up encryption.".into()) + } + } + "pending" => ("Waiting for approval", "Open Settings → Encryption on an approved device and compare the code below before approving.".into()), + "locked" => ("Unlock this device", format!("Secure key storage is unavailable: {reason}")), + "recoveryConfirmationRequired" => ("Save recovery kit", "Save the recovery key and file, then confirm. Encrypted writes remain paused until confirmation.".into()), + "keyUpdateRequired" => ("Waiting for encryption keys", "A vault update or key delivery is pending. This page checks automatically.".into()), + "verificationFailed" => ("Sync paused", format!("Data could not be verified: {reason}")), + "revoked" => ("Removed", "This device was removed from the vault. Approve it again from another device to resume.".into()), + "unavailable" => ("Not available", reason), + _ => ("Unknown", String::new()), + } +} + +impl EncryptionPage { + pub fn new(state: Entity, cx: &mut Context) -> Self { + let observe = cx.observe(&state, |_, _, cx| cx.notify()); + let mut page = Self { + state, + status: Loadable::Idle, + pending: Vec::new(), + kit: None, + kit_copied: false, + prompt: None, + error: None, + busy: false, + load_task: None, + action_task: None, + copy_task: None, + poll_task: None, + _observe: observe, + }; + page.load(cx); + page.poll_task = Some(cx.spawn(async move |this, cx| { + loop { + cx.background_executor() + .timer(std::time::Duration::from_secs(2)) + .await; + if this + .update(cx, |page, cx| { + if !page.busy { + page.load(cx); + } + }) + .is_err() + { + break; + } + } + })); + page + } + + /// `VaultRefresh` (network reconcile + status) and, when this device is + /// an approved member, the pending enrollment requests. + fn load(&mut self, cx: &mut Context) { + if self.load_task.is_some() { + return; + } + let Some(engine) = self.state.read(cx).engine().cloned() else { + return; + }; + if matches!(self.status, Loadable::Idle) { + self.status = Loadable::Loading; + } + self.load_task = Some(cx.spawn(async move |this, cx| { + let status = engine + .client() + .call(methods::VAULT_REFRESH, serde_json::json!({})) + .await; + let ready = status + .as_ref() + .ok() + .and_then(|s| s.get("phase").and_then(Value::as_str)) + == Some("ready"); + let pending = if ready { + engine + .client() + .call(methods::VAULT_PENDING_REQUESTS, serde_json::json!({})) + .await + .ok() + .and_then(|v| v.get("requests").and_then(Value::as_array).cloned()) + .unwrap_or_default() + } else { + Vec::new() + }; + this.update(cx, |page, cx| { + page.load_task = None; + page.status = match status { + Ok(value) => Loadable::Ready(value), + Err(err) => Loadable::Error(err.to_string()), + }; + page.pending = pending; + cx.notify(); + }) + .ok(); + })); + } + + /// Run one vault action, then reload. `after` receives the reply. + fn action( + &mut self, + method: &'static str, + params: Value, + after: impl FnOnce(&mut Self, Value) + Send + 'static, + cx: &mut Context, + ) { + let Some(engine) = self.state.read(cx).engine().cloned() else { + return; + }; + self.error = None; + self.busy = true; + cx.notify(); + self.action_task = Some(cx.spawn(async move |this, cx| { + let result = engine.client().call(method, params).await; + this.update(cx, |page, cx| { + page.busy = false; + match result { + Ok(value) => after(page, value), + Err(err) => page.error = Some(err.to_string()), + } + page.load(cx); + cx.notify(); + }) + .ok(); + })); + } + + fn setup(&mut self, cx: &mut Context) { + self.action( + methods::VAULT_SETUP, + serde_json::json!({}), + |page, value| { + let kit = value + .get("kit") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(); + let file = value + .get("recoveryFile") + .map(|f| serde_json::to_string_pretty(f).unwrap_or_default()) + .unwrap_or_default(); + page.kit = Some((kit, file)); + page.kit_copied = false; + }, + cx, + ); + } + + fn request_enrollment(&mut self, cx: &mut Context) { + self.action( + methods::VAULT_REQUEST_ENROLLMENT, + serde_json::json!({}), + |_, _| {}, + cx, + ); + } + + fn cancel_enrollment(&mut self, cx: &mut Context) { + self.action( + methods::VAULT_CANCEL_ENROLLMENT, + serde_json::json!({}), + |_, _| {}, + cx, + ); + } + + fn approve(&mut self, request_id: String, code: String, cx: &mut Context) { + self.action( + methods::VAULT_APPROVE, + serde_json::json!({ "requestId": request_id, "code": code }), + |_, _| {}, + cx, + ); + } + + fn reject(&mut self, request_id: String, cx: &mut Context) { + self.action( + methods::VAULT_REJECT, + serde_json::json!({ "requestId": request_id }), + |_, _| {}, + cx, + ); + } + + fn revoke(&mut self, device_id: String, cx: &mut Context) { + self.action( + methods::VAULT_REVOKE, + serde_json::json!({ "deviceId": device_id }), + |_, _| {}, + cx, + ); + } + + fn open_rename(&mut self, device_id: String, current: String, cx: &mut Context) { + let input = cx.new(|cx| ComposerInput::new("Device name", cx)); + input.update(cx, |input, cx| input.set_text(current, cx)); + let events = cx.subscribe(&input, |this: &mut Self, _, event, cx| { + if matches!(event, ComposerInputEvent::Submitted) { + this.submit_prompt(cx); + } + }); + self.prompt = Some(PromptDialog { + kind: Prompt::Rename { device_id }, + input, + _events: events, + }); + cx.notify(); + } + + fn open_recover(&mut self, cx: &mut Context) { + let input = cx.new(|cx| ComposerInput::new("Recovery key (XXXXX-XXXXX-…)", cx)); + let events = cx.subscribe(&input, |this: &mut Self, _, event, cx| { + if matches!(event, ComposerInputEvent::Submitted) { + this.submit_prompt(cx); + } + }); + self.prompt = Some(PromptDialog { + kind: Prompt::Recover, + input, + _events: events, + }); + cx.notify(); + } + + fn submit_prompt(&mut self, cx: &mut Context) { + let Some(dialog) = self.prompt.take() else { + return; + }; + let text = dialog.input.read(cx).text().trim().to_string(); + if text.is_empty() { + cx.notify(); + return; + } + match dialog.kind { + Prompt::Rename { device_id } => self.action( + methods::VAULT_RENAME_DEVICE, + serde_json::json!({"deviceId": device_id, "name": text}), + |_, _| {}, + cx, + ), + Prompt::Recover => self.action( + methods::VAULT_RECOVER, + serde_json::json!({ "kit": text }), + |_, _| {}, + cx, + ), + } + } + + fn copy_kit(&mut self, cx: &mut Context) { + let Some((kit, file)) = self.kit.clone() else { + return; + }; + cx.write_to_clipboard(ClipboardItem::new_string(format!( + "Zeron recovery key: {kit}\n\nRecovery file:\n{file}\n" + ))); + self.kit_copied = true; + self.copy_task = Some(cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(1500)) + .await; + this.update(cx, |page, cx| { + page.kit_copied = false; + cx.notify(); + }) + .ok(); + })); + cx.notify(); + } + + fn render_prompt( + &mut self, + viewport: gpui::Size, + cx: &mut Context, + ) -> Option { + let theme = Theme::of(cx).clone(); + let dialog = self.prompt.as_ref()?; + let input = dialog.input.clone(); + let (title, description, submit) = match &dialog.kind { + Prompt::Recover => ( + "Use recovery key", + "Enter the recovery key you saved when you set up encryption. This adds this device under a fresh key epoch; other devices catch up automatically.", + "Recover", + ), + Prompt::Rename { .. } => ( + "Rename device", + "Choose a recognizable name, like Laptop or iPhone. Names sync across your approved devices.", + "Save", + ), + }; + let card = popover::dialog_card(&theme) + .child(popover::dialog_title(&theme, title)) + .child( + div() + .mt(px(8.0)) + .text_size(crate::typography::ui_rems(12.5)) + .text_color(theme.text_muted) + .child(SharedString::from(description)), + ) + .child( + div() + .mt(px(12.0)) + .child(popover::dialog_field(input.into_any_element())), + ) + .child( + div() + .mt(px(16.0)) + .flex() + .flex_row() + .justify_end() + .gap(px(8.0)) + .child( + popover::btn_ghost(&theme, "Cancel", "vault-prompt-cancel") + .id("vault-prompt-cancel") + .on_click(cx.listener(|this, _, _, cx| { + this.prompt = None; + cx.notify(); + })), + ) + .child( + popover::btn_primary(&theme, submit) + .id("vault-prompt-submit") + .on_click(cx.listener(|this, _, _, cx| this.submit_prompt(cx))), + ), + ) + .into_any_element(); + Some(popover::modal("vault-prompt-dialog", viewport, card)) + } + + fn action_button( + &self, + theme: &Theme, + id: &'static str, + label: &'static str, + primary: bool, + cx: &mut Context, + on_click: impl Fn(&mut Self, &mut Context) + 'static, + ) -> AnyElement { + let busy = self.busy; + let button = if primary { + popover::btn_primary(theme, label) + } else { + popover::btn_ghost(theme, label, id) + }; + button + .id(id) + .when(busy, |el| el.opacity(0.5)) + .when(!busy, |el| { + el.cursor_pointer() + .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) + }) + .into_any_element() + } + + fn render_status(&mut self, theme: &Theme, cx: &mut Context) -> AnyElement { + let Loadable::Ready(status) = &self.status else { + return widgets::section_card(theme) + .p(px(16.0)) + .child(popover::skeleton_rows( + "vault-skeleton", + theme, + 2, + cx.entity_id(), + cx, + )) + .into_any_element(); + }; + let status = status.clone(); + let phase = status + .get("phase") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let (title, copy) = phase_copy(&status); + let epoch = status.get("epoch").and_then(Value::as_u64); + let protection = status + .get("protection") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let mut meta: Vec = vec![ + div() + .w_full() + .min_w_0() + .whitespace_normal() + .child(SharedString::from(copy)) + .into_any_element(), + ]; + if let Some(epoch) = epoch { + meta.push( + div() + .child(SharedString::from(format!( + "Key epoch {epoch} · keys protected by {}", + match protection.as_str() { + "keychain" => "the macOS Keychain", + "systemdCredential" => "a systemd credential (unattended)", + "keyFile" => "an operator key file (unattended)", + _ => "this process only", + } + ))) + .into_any_element(), + ); + } + let mut actions: Vec = Vec::new(); + match phase.as_str() { + "recoveryConfirmationRequired" => { + actions.push(self.action_button( + theme, + "vault-show-recovery", + "Show recovery kit", + true, + cx, + |this, cx| this.setup(cx), + )); + } + "notEnrolled" => { + if status.get("remoteVault").and_then(Value::as_bool) == Some(true) { + actions.push(self.action_button( + theme, + "vault-request", + "Approve from another device", + true, + cx, + |this, cx| this.request_enrollment(cx), + )); + actions.push(self.action_button( + theme, + "vault-recover", + "Use recovery key", + false, + cx, + |this, cx| this.open_recover(cx), + )); + } else { + actions.push(self.action_button( + theme, + "vault-setup", + "Set up encryption", + true, + cx, + |this, cx| this.setup(cx), + )); + } + } + "pending" => { + actions.push(self.action_button( + theme, + "vault-cancel", + "Cancel request", + false, + cx, + |this, cx| this.cancel_enrollment(cx), + )); + } + "revoked" => { + actions.push(self.action_button( + theme, + "vault-request-again", + "Approve from another device", + true, + cx, + |this, cx| this.request_enrollment(cx), + )); + } + _ => {} + } + if let Some(fingerprint) = status.get("genesisHash").and_then(Value::as_str) { + let fingerprint = fingerprint.to_string(); + actions.push(self.action_button( + theme, + "vault-copy-fingerprint", + "Copy vault fingerprint", + false, + cx, + move |_, cx| cx.write_to_clipboard(ClipboardItem::new_string(fingerprint.clone())), + )); + } + actions.push(self.action_button( + theme, + "vault-refresh", + "Refresh", + false, + cx, + |this, cx| this.load(cx), + )); + let badge = match phase.as_str() { + "ready" => widgets::badge_active(theme, "On"), + _ => widgets::badge(theme, title), + }; + let mut card = widgets::section_card(theme).child( + widgets::card_row(theme, true) + .child(widgets::row_tile(theme, crate::icons::KEY_MINIMALISTIC)) + .child( + div() + .flex_1() + .min_w_0() + .flex() + .flex_col() + .child(widgets::row_title(theme, "End-to-end encryption")) + .child( + div() + .w_full() + .min_w_0() + .flex() + .flex_col() + .gap(px(4.0)) + .text_size(crate::typography::ui_rems(12.0)) + .text_color(theme.text_muted) + .children(meta), + ), + ) + .child(badge), + ); + if phase == "pending" + && let Some(code) = status.get("pairingCode").and_then(Value::as_str) + { + card = card.child( + widgets::card_row(theme, false).child( + div() + .flex_1() + .flex() + .flex_col() + .gap(px(4.0)) + .child(widgets::row_title(theme, "Comparison code")) + .child( + div() + .font_family("Geist Mono") + .text_size(crate::typography::ui_rems(22.0)) + .text_color(theme.text) + .child(SharedString::from(code.to_string())), + ) + .child(widgets::meta_line( + theme, + vec![div() + .w_full().min_w_0().whitespace_normal() + .child(SharedString::from( + "Approve only if the approving device shows exactly this code.", + )) + .into_any_element()], + )), + ), + ); + } + card = card.child( + widgets::card_row(theme, false).child( + div() + .flex_1() + .flex() + .flex_row() + .flex_wrap() + .gap(px(8.0)) + .children(actions), + ), + ); + card.into_any_element() + } + + fn render_migration(&self, theme: &Theme, cx: &mut Context) -> Option { + let Loadable::Ready(status) = &self.status else { + return None; + }; + if status.get("phase").and_then(Value::as_str) != Some("ready") { + return None; + } + let migration = status.get("migration")?; + let phase = migration.get("phase").and_then(Value::as_str).unwrap_or(""); + let done = migration + .get("completed") + .and_then(Value::as_u64) + .unwrap_or(0); + let total = migration.get("total").and_then(Value::as_u64).unwrap_or(0); + let title = match phase { + "copying" => format!("Encrypting existing chats · {done} of {total}"), + "complete" => "Chat history is up to date".into(), + "paused" => format!("History migration paused · {done} of {total}"), + _ => "Preparing existing chat history".into(), + }; + let mut content = div().flex().flex_col().gap(px(8.0)).min_w_0().flex_1() + .child(widgets::row_title(theme, title)) + .child(div().w_full().whitespace_normal().text_size(crate::typography::ui_rems(12.0)) + .text_color(theme.text_muted).child("Original plaintext copies are retained. Unsynced history on another device is copied when that device reconnects and joins encryption.")); + if let Some(error) = migration.get("error").and_then(Value::as_str) { + content = content.child( + div() + .w_full() + .whitespace_normal() + .text_size(crate::typography::ui_rems(12.0)) + .text_color(theme.danger) + .child(error.to_owned()), + ); + } + let mut row = widgets::card_row(theme, true).child(content); + if phase != "copying" { + row = row.child(self.action_button( + theme, + "vault-migrate", + "Retry / check history", + false, + cx, + |page, cx| { + page.action( + methods::VAULT_MIGRATE_HISTORY, + serde_json::json!({}), + |_, _| {}, + cx, + ) + }, + )); + } + Some(widgets::section_card(theme).child(row).into_any_element()) + } + + fn render_kit(&mut self, theme: &Theme, cx: &mut Context) -> Option { + let (kit, _) = self.kit.clone()?; + let copied = self.kit_copied; + Some( + widgets::section_card(theme) + .child( + widgets::card_row(theme, true).child( + div() + .flex_1() + .flex() + .flex_col() + .gap(px(6.0)) + .child(widgets::row_title(theme, "Save your recovery key now")) + .child( + div() + .font_family("Geist Mono") + .text_size(crate::typography::ui_rems(13.0)) + .text_color(theme.text) + .child(SharedString::from(kit)), + ) + .child(widgets::warning_strip( + theme, + "If you lose every approved device and your recovery key, we \ + cannot recover your encrypted data. Resetting your account \ + password will not restore access.", + )) + .child( + div() + .flex() + .flex_row() + .gap(px(8.0)) + .child( + popover::btn_primary( + theme, + if copied { + "Copied" + } else { + "Copy key and file" + }, + ) + .id("vault-kit-copy") + .cursor_pointer() + .on_click(cx.listener(|this, _, _, cx| this.copy_kit(cx))), + ) + .child( + popover::btn_ghost(theme, "I saved it", "vault-kit-done") + .id("vault-kit-done") + .cursor_pointer() + .on_click(cx.listener(|this, _, _, cx| { + this.action( + methods::VAULT_CONFIRM_RECOVERY, + serde_json::json!({}), + |page, _| { + page.kit = None; + }, + cx, + ); + })), + ), + ), + ), + ) + .into_any_element(), + ) + } + + fn render_pending(&mut self, theme: &Theme, cx: &mut Context) -> Option { + if self.pending.is_empty() { + return None; + } + let rows: Vec = self + .pending + .iter() + .enumerate() + .map(|(ix, request)| { + let request_id = request + .get("requestId") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let code = request + .get("pairingCode") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let device = request + .get("deviceId") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let approve_id = request_id.clone(); + let approve_code = code.clone(); + let reject_id = request_id.clone(); + widgets::card_row(theme, ix == 0) + .id(("vault-pending", ix)) + .child( + div() + .flex_1() + .min_w_0() + .flex() + .flex_col() + .child(widgets::row_title( + theme, + format!("Device {}", crate::settings::devices::short_id(&device)), + )) + .child( + div() + .font_family("Geist Mono") + .text_size(crate::typography::ui_rems(18.0)) + .text_color(theme.text) + .child(SharedString::from(code.clone())), + ) + .child(widgets::meta_line( + theme, + vec![ + div() + .w_full() + .min_w_0() + .whitespace_normal() + .child(SharedString::from( + "Compare with the code on the new device. An approved \ + device can read all synced content and manage devices.", + )) + .into_any_element(), + ], + )), + ) + .child(self.action_button( + theme, + "vault-reject", + "Reject", + false, + cx, + move |this, cx| this.reject(reject_id.clone(), cx), + )) + .child(self.action_button( + theme, + "vault-approve", + "Codes match — approve", + true, + cx, + move |this, cx| this.approve(approve_id.clone(), approve_code.clone(), cx), + )) + .into_any_element() + }) + .collect(); + Some( + div() + .flex() + .flex_col() + .gap(px(8.0)) + .mt(px(24.0)) + .child(widgets::field_label(theme, "Devices waiting for approval")) + .child(widgets::section_card(theme).mt(px(0.0)).children(rows)) + .into_any_element(), + ) + } + + fn render_devices(&mut self, theme: &Theme, cx: &mut Context) -> Option { + let Loadable::Ready(status) = &self.status else { + return None; + }; + let devices = status.get("devices")?.as_array()?.clone(); + if devices.is_empty() { + return None; + } + let rows: Vec = devices + .iter() + .enumerate() + .map(|(ix, device)| { + let id = device + .get("deviceId") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let active = device.get("status").and_then(Value::as_str) == Some("active"); + let this_device = device.get("thisDevice").and_then(Value::as_bool) == Some(true); + let state = self.state.read(cx); + let named = state.devices.iter().find(|d| { + d.vault_device_id.as_deref() == Some(id.as_str()) + || (this_device && state.local_device_id.as_deref() == Some(d.id.as_str())) + }); + let label = status + .get("deviceNames") + .and_then(|names| names.get(&id)) + .and_then(Value::as_str) + .map(str::to_owned) + .or_else(|| named.map(|d| d.name.clone())) + .unwrap_or_else(|| { + if this_device { + "This computer".into() + } else { + format!("Device {}", crate::settings::devices::short_id(&id)) + } + }); + let icon = if named.is_some_and(|d| d.platform == "ios") { + crate::icons::SMARTPHONE + } else { + crate::icons::MONITOR + }; + let revoke_id = id.clone(); + let rename_id = id.clone(); + let rename_name = label.clone(); + let mut row = widgets::card_row(theme, ix == 0) + .id(("vault-device", ix)) + .when(!active, |el| el.opacity(0.55)) + .child(widgets::row_tile(theme, icon)) + .child( + div() + .flex_1() + .min_w_0() + .flex() + .flex_col() + .child(widgets::row_title(theme, label)) + .child(widgets::meta_line( + theme, + vec![ + div() + .child(SharedString::from(if active { + "Approved" + } else { + "Removed" + })) + .into_any_element(), + ], + )), + ); + if status.get("phase").and_then(Value::as_str) == Some("ready") { + row = row.child(self.action_button( + theme, + "vault-rename", + "Rename", + false, + cx, + move |this, cx| { + this.open_rename(rename_id.clone(), rename_name.clone(), cx) + }, + )); + } + if this_device { + row = row.child(widgets::badge_active(theme, "This device")); + } else if active { + row = row.child(self.action_button( + theme, + "vault-revoke", + "Remove", + false, + cx, + move |this, cx| this.revoke(revoke_id.clone(), cx), + )); + } + row.into_any_element() + }) + .collect(); + Some( + div() + .flex() + .flex_col() + .gap(px(8.0)) + .mt(px(24.0)) + .child(widgets::field_label(theme, "Approved devices")) + .child(widgets::section_card(theme).mt(px(0.0)).children(rows)) + .child(widgets::meta_line( + theme, + vec![ + div() + .w_full() + .min_w_0() + .whitespace_normal() + .child(SharedString::from( + "Removing a device stops its future sync access after the change \ + takes effect. It cannot erase information the device already \ + downloaded.", + )) + .into_any_element(), + ], + )) + .into_any_element(), + ) + } +} + +impl Render for EncryptionPage { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = Theme::of(cx).clone(); + let scope = self.state.read(cx).workspace_scope; + let error = self + .error + .clone() + .map(|message| widgets::error_strip(&theme, message).into_any_element()); + let load_error = match &self.status { + Loadable::Error(message) => Some(message.clone()), + _ => None, + }; + let subtitle = match scope { + Some(WorkspaceScope::Local) => { + "This workspace is local-only; nothing is sent to a sync backend.".to_string() + } + _ => "Manage the devices that can read your encrypted sessions and files. Existing history is copied into encryption after setup; original plaintext copies are retained." + .to_string(), + }; + let status = self.render_status(&theme, cx); + let migration = self.render_migration(&theme, cx); + let kit = self.render_kit(&theme, cx); + let pending = self.render_pending(&theme, cx); + let devices = self.render_devices(&theme, cx); + let dialog = self.render_prompt(window.viewport_size(), cx); + + div() + .id("encryption-page") + .size_full() + .overflow_y_scroll() + .child( + widgets::page_column() + .child(widgets::page_header(&theme, "Encryption", None)) + .child( + widgets::page_subtitle(&theme, subtitle) + .max_w(px(512.0)) + .line_height(px(20.0)), + ) + .children(error) + .when_some(load_error, |el, message| { + el.child(widgets::error_strip(&theme, message)) + }) + .children(kit) + .child(status) + .children(migration) + .children(pending) + .children(devices), + ) + .when_some(dialog, |el, dialog| el.child(dialog)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn phase_copy_never_promises_encryption_before_ready() { + let not_enrolled = serde_json::json!({ "phase": "notEnrolled", "remoteVault": false }); + assert_eq!(phase_copy(¬_enrolled).0, "Not set up"); + let existing = serde_json::json!({ "phase": "notEnrolled", "remoteVault": true }); + assert_eq!(phase_copy(&existing).0, "Approve this device"); + let locked = serde_json::json!({ "phase": "locked", "reason": "no keychain" }); + assert!(phase_copy(&locked).1.contains("no keychain")); + assert_eq!( + phase_copy(&serde_json::json!({ "phase": "ready" })).0, + "Vault ready" + ); + assert_eq!( + phase_copy(&serde_json::json!({ "phase": "keyUpdateRequired" })).0, + "Waiting for encryption keys" + ); + } +} diff --git a/crates/ui/src/settings/notifications.rs b/crates/ui/src/settings/notifications.rs index e7ef010de..3262d19ee 100644 --- a/crates/ui/src/settings/notifications.rs +++ b/crates/ui/src/settings/notifications.rs @@ -102,8 +102,7 @@ impl Render for NotificationsPage { vec![ div() .child(SharedString::from( - "Show a system banner on the same events, so pings \ - reach you while Zeron is in the background.", + "Show system banners for session updates and device approval requests.", )) .into_any_element(), ], @@ -132,14 +131,13 @@ impl Render for NotificationsPage { .min_w_0() .flex() .flex_col() - .child(widgets::row_title(&theme, "Only when in the background")) + .child(widgets::row_title(&theme, "Session updates only in background")) .child(widgets::meta_line( &theme, vec![ div() .child(SharedString::from( - "Skip the banner while a Zeron window is focused — \ - the chime already covers it.", + "Skip session banners while Zeron is focused. Device approval requests still notify.", )) .into_any_element(), ], diff --git a/crates/ui/src/shell.rs b/crates/ui/src/shell.rs index 655b662c2..45fb83518 100644 --- a/crates/ui/src/shell.rs +++ b/crates/ui/src/shell.rs @@ -373,6 +373,8 @@ pub fn apply_keymap( #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SettingsSection { Devices, + /// End-to-end encrypted sync: vault state, pairing, recovery (RFC 0001). + Encryption, /// Which harnesses the composer offers (enable/disable toggles). Harnesses, /// Per-provider CLI accounts (login, usage) — labeled "Accounts". @@ -385,8 +387,9 @@ pub enum SettingsSection { } impl SettingsSection { - pub const ALL: [SettingsSection; 8] = [ + pub const ALL: [SettingsSection; 9] = [ SettingsSection::Devices, + SettingsSection::Encryption, SettingsSection::Harnesses, SettingsSection::Agents, SettingsSection::Appearance, @@ -401,6 +404,7 @@ impl SettingsSection { pub fn label(self) -> &'static str { match self { SettingsSection::Devices => "Devices", + SettingsSection::Encryption => "Encryption", SettingsSection::Harnesses => "Agents", SettingsSection::Agents => "Accounts", SettingsSection::Appearance => "Appearance", @@ -1186,6 +1190,7 @@ pub struct Shell { shortcuts_page: Option>, accounts_page: Option>, harnesses_page: Option>, + encryption_page: Option>, shortcuts_sub: Option, notifications_sub: Option, files_settings_sub: Option, @@ -1222,6 +1227,9 @@ pub struct Shell { user_menu: popover::Popup<()>, /// Inline sidebar error strip (mutation failures); click dismisses. sidebar_notice: Option, + vault_requests_task: Option>, + vault_requests_seen: crate::notify::ApprovalRequests, + vault_requests_pending: usize, /// Local lifecycle of an in-app update (macOS bundle swap) — the engine's /// UpdateStatus stream says WHETHER one exists; this says how far the /// download/stage of it has come in this process. @@ -1386,12 +1394,17 @@ impl Shell { // live so elapsed time and the flavour word stay fresh. let ticker = cx.spawn(async move |this, cx| { let mut displayed_minute = Utc::now().timestamp().div_euclid(60); + let mut vault_tick = 0u8; loop { cx.background_executor().timer(Duration::from_secs(1)).await; let minute = Utc::now().timestamp().div_euclid(60); let minute_changed = minute != displayed_minute; displayed_minute = minute; let alive = this.update(cx, |shell: &mut Shell, cx| { + if vault_tick == 0 { + shell.poll_vault_requests(cx); + } + vault_tick = (vault_tick + 1) % 5; let live = { let s = shell.state.read(cx); s.selected_chat @@ -1432,6 +1445,7 @@ impl Shell { } Some("settings/agents") => Route::Settings(SettingsSection::Agents), Some("settings/harnesses") => Route::Settings(SettingsSection::Harnesses), + Some("settings/encryption") => Route::Settings(SettingsSection::Encryption), Some("settings/appearance") => Route::Settings(SettingsSection::Appearance), Some("settings/notifications") => Route::Settings(SettingsSection::Notifications), Some("settings/shortcuts") => Route::Settings(SettingsSection::Shortcuts), @@ -1525,6 +1539,7 @@ impl Shell { shortcuts_page: None, accounts_page: None, harnesses_page: None, + encryption_page: None, shortcuts_sub: None, notifications_sub: None, files_settings_sub: None, @@ -1544,6 +1559,9 @@ impl Shell { sound_prev: std::collections::HashMap::new(), user_menu: popover::Popup::default(), sidebar_notice: None, + vault_requests_task: None, + vault_requests_seen: Default::default(), + vault_requests_pending: 0, update_flow: UpdateFlow::Idle, update_task: None, update_dismissed: None, @@ -3077,6 +3095,52 @@ impl Shell { cx.notify(); } + /// Approval requests are app-wide: settings need not be open to discover them. + fn poll_vault_requests(&mut self, cx: &mut Context) { + if self.vault_requests_task.is_some() { + return; + } + let Some(engine) = self.state.read(cx).engine().cloned() else { + if self.vault_requests_pending != 0 { + self.vault_requests_pending = 0; + cx.notify(); + } + return; + }; + let scope = self.state.read(cx).workspace_scope.clone(); + self.vault_requests_task = Some(cx.spawn(async move |this, cx| { + let result = async { + let status = engine.client().call(methods::VAULT_REFRESH, serde_json::json!({})).await?; + if status.get("phase").and_then(serde_json::Value::as_str) != Some("ready") { + return Ok::<_, zeron_rpc::RpcError>((String::new(), Vec::new())); + } + let vault = status.get("genesisHash").and_then(serde_json::Value::as_str).unwrap_or("").to_owned(); + let response = engine.client().call(methods::VAULT_PENDING_REQUESTS, serde_json::json!({})).await?; + let requests = response.get("requests").and_then(serde_json::Value::as_array).cloned().unwrap_or_default(); + Ok((vault, requests)) + }.await; + this.update(cx, |shell, cx| { + shell.vault_requests_task = None; + if shell.state.read(cx).workspace_scope != scope || shell.state.read(cx).engine().is_none() { + shell.vault_requests_pending = 0; + cx.notify(); + return; + } + if let Ok((vault, requests)) = result { + shell.vault_requests_pending = requests.len(); + let new = shell.vault_requests_seen.observe(&vault, requests.iter().filter_map(|request| + request.get("requestId").and_then(serde_json::Value::as_str))); + if new && shell.settings.notifications_enabled { + // Approval prompts remain useful while the app is focused; + // the background-only preference applies to session updates. + crate::notify::post("Device approval requested", "A device wants access to your encrypted vault. Open Settings → Encryption to compare its code and review the request."); + } + cx.notify(); + } + }).ok(); + })); + } + fn open_settings(&mut self, section: SettingsSection, cx: &mut Context) { // Recreate per visit: the page's ListHarnesses load re-probes which // CLIs are installed, so installing one shows up on the next open. @@ -3159,6 +3223,18 @@ impl Shell { None => Empty.into_any_element(), } } + SettingsSection::Encryption => { + if self.encryption_page.is_none() { + let state = self.state.clone(); + self.encryption_page = Some( + cx.new(|cx| crate::settings::encryption::EncryptionPage::new(state, cx)), + ); + } + match &self.encryption_page { + Some(page) => page.clone().into_any_element(), + None => Empty.into_any_element(), + } + } SettingsSection::Agents => { if self.accounts_page.is_none() { let state = self.state.clone(); @@ -4566,6 +4642,7 @@ impl Shell { let section_icon = |item: SettingsSection| match item { SettingsSection::Devices => icons::MONITOR, SettingsSection::Harnesses => icons::WIDGET, + SettingsSection::Encryption => icons::KEY_MINIMALISTIC, SettingsSection::Agents => icons::KEY_MINIMALISTIC, SettingsSection::Appearance => icons::TUNING, SettingsSection::Files => icons::FOLDER, @@ -5269,6 +5346,35 @@ impl Shell { .when_some(self.render_update_strip(theme, cx), |el, strip| { el.child(strip) }) + .when(self.vault_requests_pending > 0, |el| { + el.child( + div() + .id("vault-approval-notice") + .mx(px(Theme::SPACE_SM)) + .mt(px(4.0)) + .px(px(Theme::SPACE_SM)) + .py(px(6.0)) + .rounded(px(Theme::CONTROL_RADIUS)) + .bg(theme.accent_wash) + .hover(move |s| s.bg(theme.accent.opacity(0.16))) + .text_size(crate::typography::ui_rems(11.0)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(theme.accent) + .cursor_pointer() + .on_click(cx.listener(|this, _, _, cx| { + this.open_settings(SettingsSection::Encryption, cx) + })) + .child(format!( + "{} device approval request{} · Review", + self.vault_requests_pending, + if self.vault_requests_pending == 1 { + "" + } else { + "s" + } + )), + ) + }) // Inline mutation-failure notice. .when_some(self.sidebar_notice.clone(), |el, notice| { el.child( diff --git a/crates/ui/src/state.rs b/crates/ui/src/state.rs index 87206e936..c896010af 100644 --- a/crates/ui/src/state.rs +++ b/crates/ui/src/state.rs @@ -3033,6 +3033,7 @@ mod tests { fn device(id: &str, name: &str) -> Device { Device { + vault_device_id: None, id: id.into(), name: name.into(), platform: "macos".into(), @@ -3892,6 +3893,7 @@ mod tests { "unknown device conservatively fails the gate" ); s.devices = vec![Device { + vault_device_id: None, id: "d1".into(), name: "laptop".into(), platform: "macos".into(), @@ -3949,6 +3951,7 @@ mod tests { local.device_id = "local".into(); s.chats = vec![remote, local]; s.devices = vec![Device { + vault_device_id: None, id: "remote".into(), name: "vps".into(), platform: "linux".into(), diff --git a/docs/encrypted-sync-plan.txt b/docs/encrypted-sync-plan.txt new file mode 100644 index 000000000..679ff5ef3 --- /dev/null +++ b/docs/encrypted-sync-plan.txt @@ -0,0 +1,1920 @@ +Zeron Project September 2026 +Project Plan: Encrypted Sync Date: 2026-09-04 +Status: Implementation in progress / encrypted-sync activation unavailable +Branch: feat/encrypted-sync +Design: docs/rfc-0001-encrypted-sync.txt (Draft 00) +Code baseline: a0650ab + + + ENCRYPTED SYNC: DELIVERY PROJECT PLAN + ==================================== + +1. Outcome and Boundaries +-------------------------- + + Deliver opt-in end-to-end encrypted sync for one personal account + vault per (orgId, userId). This is PER USER WITHIN AN ORGANISATION, + not per project, not per chat, and not shared with other organisation + members. All projects/spaces in that profile belong to the same vault. + + Success means a user can: + + - Enable encryption without losing sessions or queued commands. + - Approve a desktop, phone, or headless host once, then use normal + streaming, offline reads, queued sends, and remote control. + - Recover using the saved recovery kit without an existing device. + - Remove a device and protect subsequent writes through key rotation. + - Understand retained legacy copies and endpoint/metadata limits. + + The backend receives ciphertext on every content-bearing path. Login + alone cannot unlock the vault. Every approved device, including a VPS, + is a full-trust peer in v1; restricted runners are not part of this plan. + + Non-goals: per-project grants, organisation sharing, server key escrow, + local-at-rest encryption, model-provider confidentiality, traffic hiding, + guaranteed erasure of previous copies, or plaintext downgrade. + + The RFC is the security contract; this plan organises delivery. If an + implementation needs to change that contract, amend/review the RFC first. + Existing user changes are not part of the project merely because they + are present on this branch. Do not bundle unrelated iOS work into PRs. + + +2. Current Status and Working Rules +----------------------------------- + + Existing artefacts: + + [x] Draft RFC written and repository data paths investigated. + [x] Per-user, per-organisation scope clarified with the maintainer. + [x] Dedicated feature branch created from the current main checkout. + [ ] RFC protocol gates G1-G6 resolved and recorded. + [ ] Implementation, migration, and deployment. + + All ES work packages below start PENDING. A written design is not an + implemented control, and a passing crypto test is not complete sync. + + Maintainer direction (2026-09-04): + - Commit and push notable progress to PR #252 and keep its status + updated without waiting for another commit/push request. + - Deliver encrypted sync as one feature in this PR. No encryption + format has shipped; draft codecs may be replaced directly. Do not + add compatibility layers or migrations for earlier draft formats. + - This does not remove the need to migrate existing plaintext data, + stop old plaintext producers, or preserve user data. + - Fix the pinned GPUI dependency setup and use normal workspace tests + rather than treating an offline checkout failure as permanent. + - Proceed with implementation using idiomatic defaults. Security + review and complete coverage remain release gates, not a reason + to preserve unshipped prototypes or postpone implementation. + + Before starting a package: + + 1. Assign one accountable owner and a reviewer; roles below are + planning labels, not assignments to particular people. + 2. Confirm dependencies and record exact design decisions for the + slice before coding. Track unresolved protocol/security risks as + blockers to production activation, with tests and review evidence. + 3. Capture the existing test baseline and write the failing fixture + or regression test for the behaviour being introduced. + 4. Keep changes narrow and preserve existing comments/conventions. + 5. Attach acceptance evidence to the PR; update package status only + after that evidence is reviewed. + + New cryptographic dependencies require provenance, age, build/install, + and source review before installation or execution. Do not weaken + repository security controls or use floating latest versions to unblock + a build. Library selection and independent security review are explicit + work, not assumptions hidden in a coding task. + + Milestone commits/PR updates and restoring pinned build dependencies + are authorised by the maintainer's current direction above. Production + requests, data deletion, and release still require separate approval. + New dependencies still require supply-chain review. Historical discovery + entries below describe what had run at each point, not the current limit. + + +3. Milestones and Dependency Order +---------------------------------- + + Milestone Required packages Observable exit + ---------- ----------------------- --------------------------------- + M0 Design ES-01, ES-02 Reviewed protocol decisions and + complete content/legacy inventory. + + M1 Access ES-03 through ES-07 Two native clients can enrol and + recover a synthetic vault; a + headless host can restart safely. + + M2 Chats ES-08, ES-09 Encrypted chat rows/checkpoints + survive disconnects and restores. + + M3 Coverage ES-10 through ES-13 Registry, RPC, sidecars, blobs, + and rotation work end to end. + + M4 Upgrade ES-14 A fixture legacy account migrates + without lost or replayed work. + + M5 Release ES-15, ES-16 Security/reliability evidence and + an approved staged rollout. + + M1 and M2 are test milestones, not public encrypted-sync releases. + The user-facing activation switch remains unavailable until M5 gates + are satisfied. Internal tests use synthetic, explicitly isolated data. + + Dependency table (numbers refer to ES packages): + + 01: none 09: 02, 08 + 02: none 10: 02, 03, 04, 05 + 03: 01, 02 11: 03, 04, 05, 06 + 04: 01, 02, 03 12: 09, 10, 11 + 05: 01, 02, 03 13: 05, 06, 08, 09, 10, 11, 12 + 06: 03, 04, 05 14: 02, 07, 13 + 07: 04, 06 15: 07, 14 + 08: 03, 04, 05 16: 15 + + Practical sequence: + + 01 + 02 -> 03 -> 04 + 05 -> 06 -> 07 + | | + | +-> 11 --+ + +-> 08 -> 09 ---+-> 12 -> 13 -> 14 + +-> 10 --------+ | + 15 -> 16 + + The dependency table is authoritative. Parallel work is possible on + independent adapters and test fixtures once shared interfaces are + frozen. Do not parallelise incompatible edits to shared protocol or + schema files. Bootstrap, registry lifecycle proofs, and migration + ownership mapping are distinct designs, not one large transport patch. + + +4. Work Packages +----------------- + +ES-01 Resolve protocol and trust decisions + + Role: Protocol/security lead, with Rust and Swift reviewers. + Depends: None. RFC: Sections 5-7, 11, 15; gates G1-G3 and crypto part G5. + + Deliver: + - Freeze pairing/channel construction, cipher suite, canonical + records, signature/AAD inputs, key labels, and nonce allocation. + - Specify membership authority, genesis pins, concurrent changes, + recovery-authority replacement, and accepted-history boundaries. + - Specify crash/clone-safe key usage, immutable checkpoint identity, + epoch activation, revocation freshness limits, and test vectors. + - Select/vet compatible libraries; decide module placement. Prefer + a small reusable Rust crypto boundary shared by sync and RPC, + with an interoperable Swift counterpart. No parallel protocols. + + Acceptance: + - Every G1-G3 decision has a recorded answer, reviewer, and negative + test case; key/epoch dependencies have no bootstrap cycle. + - A protocol reviewer can explain how key substitution, replay, + nonce reuse, stale recovery, and revocation are handled or limited. + - No implementation starts by inventing unspecified crypto details. + +ES-02 Inventory surfaces, schemas, legacy ownership, and baselines + + Role: Engine/edge lead with platform and privacy reviewers. + Depends: None; coordinates with ES-01. RFC: Sections 3, 9, 12-13; G4-G6. + + Deliver: + - A route/publisher/reader/storage matrix, including legacy paths, + subagent docs/blobs, attachments, sidecars, and diagnostic sinks. + - A registry lifecycle-proof/rekey design preserving HLC semantics. + - A profile ownership/fencing design for routes that currently carry + only chat, user, or installation device IDs; identify ambiguity. + - Secure-store/restore policy and safe local schema/downgrade range. + - Existing test results, synthetic thin-link/streaming baselines, + and reviewed latency, memory, frame-size, and byte budgets. + - A deployment-retention investigation plan. Use approved read-only + access for actual verification; mark unknown retention as unknown. + + Acceptance: + - Every inventory row has an implementation package and test owner. + - G4/G5 designs and G6 route/fencing design are reviewed. Deployment + retention evidence may remain open until ES-14, blocking release. + - No inferred organisation ownership or assumption that server + rejection prevents a legacy client from sending plaintext. + +ES-03 Implement shared record codecs and cross-language fixtures + + Role: Rust crypto and Swift platform owners. + Depends: ES-01, ES-02. RFC: Section 7. + Seams: Proposed reusable Rust module/crate; Swift Sync crypto module. + + Deliver: + - Seal/open, signature verification, HPKE envelopes, manifest and + membership codecs, key types, and structured failure results. + - Fixed synthetic positive/negative fixtures consumed by both + languages. Include keyring, recovery, field, row, and blob forms. + - Bounded parsing, key/nonce usage checks, deterministic signed + encoding, and safe secret handling. No HTTP or UI in primitives. + + Acceptance: + - Rust writes are readable by Swift and vice versa; fixtures agree + byte-for-byte, including errors and canonical encoding rules. + - Wrong vault/object/kind/epoch/author, duplicate fields, corrupted + ciphertext, oversized lengths, and unknown versions fail closed. + - Crash/restore/concurrent-writer nonce rules are tested, not merely + covered by comments. No secrets in debug/serialization output. + +ES-04 Secure storage and the account-vault lifecycle gate + + Role: Engine/platform owner with macOS, iOS, and Linux review. + Depends: ES-01, ES-02, ES-03. RFC: Sections 4-5, 13; G5. + Seams: engine profile/auth/startup; proto states; iOS AppConfig/AppModel. + + Deliver: + - Per-profile device identity and protected local keyring storage. + - Explicit account, profile, vault, and transport states rather than + one connected flag. Gate content producers before serialisation. + - Sign-out, key locking, restored-device identity, account switching, + and headless unattended versus unlock-on-boot policies. + - In-memory/fault-injection secure-store adapters for tests, plus + native adapters with checked persistence/access errors. + + Acceptance: + - Login/refresh alone starts no encrypted-profile content transport. + - A locked store, denied keychain, failed write, or restored clone + does not erase keys, enrol twice, or fall back to a cleartext file. + - Local-only/profile isolation tests remain valid. The UI does not + promise local-cache encryption just because sync keys are locked. + +ES-05 Bootstrap, membership, and policy control plane + + Role: Edge owner plus native control-plane owners. + Depends: ES-01, ES-02, ES-03. RFC: Sections 5-6, 11. + Seams: edge routing/auth and a proposed vault-control store; clients. + + Deliver: + - Versioned descriptor, signed membership revision/CAS, recipient + envelopes, recovery envelopes, and staged epoch activation. + - Short-lived enrolment requests, device-possession proof, account + scoping, request limits, and minimal auditable public metadata. + - Client verification and local pins; bootstrap independent of the + registry whose keys it supplies. Explicit missing/stale states. + + Acceptance: + - Account session plus backend dump cannot unlock or approve a peer. + - Forged author, changed recipient, wrong profile, replayed change, + stale CAS, and an absent descriptor cannot reset trust. + - Crash/retry tests do not activate an epoch without durable device + and recovery envelopes. No private key or plaintext keyring on edge. + +ES-06 Pairing and recovery engines + + Role: Native protocol owner; security review required. + Depends: ES-03, ES-04, ES-05. RFC: Sections 4, 6. + Seams: Rust/Swift vault services above crypto and below UI. + + Deliver: + - QR and comparison-code pairing, explicit approval, possession + receipt, cancellation, expiry, retry, and rejected-request states. + - Recovery-kit creation/verification, restore, replacement, and + recovery-authority transition using the selected protocol. + - Publish updated recovery envelopes using the recovery public key; + ordinary devices never need to retain the offline recovery secret. + + Acceptance: + - Native A approves native B through an untrusted test relay. + - Key substitution, code mismatch, cross-vault pairing, replay, and + interrupted persistence fail without accidental enrolment. + - Restore without A works from actual saved kit/bundle bytes, not + an in-memory shortcut; a replaced kit and stale bundle are tested. + +ES-07 Desktop, iOS, and headless product flows + + Role: Desktop UI, iOS, and CLI owners. + Depends: ES-04, ES-06. RFC: Section 4. + Seams: ui settings/devices; iOS views/AppModel; apps/zeron command layer. + + Deliver: + - Setup/recovery-kit confirmation, approve-device, trusted-device + list, remove-device confirmation, and recovery screens. + - Clear states for locked, not approved, key update, verification + failure, offline, and revoked; preserve drafts/last-good content. + - Headless pairing and explicit credential mode. Full-vault and + device-management authority warning on VPS approval. + - Testable flow models; feature capability gate remains closed to + real-user activation until the release gate is met. Removal UI + may use a test model here, but its real action stays disabled + until ES-13 supplies complete cross-surface revocation. + + Acceptance: + - Rust and Swift copy/state transitions have matching meaning. + - Cancelling, sleeping, signing out, or restarting mid-flow does not + lose drafts or create hidden memberships. Accessibility/keyboard + flows work without requiring a camera or routine extra password. + - Keys and recovery secrets never enter telemetry, command arguments, + history, general RPC, or remotely captured enrolment logs. + +ES-08 Durable encrypted chat rows and verified cursor semantics + + Role: Sync/engine owner with Swift sync owner. + Depends: ES-03, ES-04, ES-05. RFC: Sections 7-8. + Seams: sync chat_client/store; engine chat2_host/doc_host; + iOS ChatRoomClient/SessionStore/DocDisk. + + Deliver: + - Encrypt once into a durable immutable batch outbox shared by WS + and HTTPS. Preserve semantic command IDs across transport retries. + - Explicit verified/durable apply outcomes replacing void row import. + - Verify author, epoch, and envelope before Loro import; persist + snapshot and receive cursor atomically, including failure paths. + - Preserve causal pending data and verified gaps across restarts. + + Acceptance: + - Missing key, failed authentication, failed disk write, unknown + version, and interleaved own ACK never skip an unread gap. + - Reconnect and HTTP fallback replay identical ciphertext/batch IDs + without duplicate commands or an unbounded crypto retry loop. + - Parent and live subagent docs use the same encrypted boundary; + local-only operation is unchanged. + +ES-09 Encrypted checkpoints, frontiers, and generation-aware backups + + Role: Sync/edge owner with Swift reader review. + Depends: ES-02, ES-08. RFC: Sections 7.4, 8, 12.3. + Seams: engine chat2_host/doc_host; edge chat-room/chat-log; + iOS checkpoint/resume path. + + Deliver: + - Sealed frontier and snapshot bound to one immutable manifest. + - Range-resumable ciphertext reads with revision/digest validators. + - All seed, threshold, reset, and rejected-row recovery publishers. + - Generation-aware backup identity and key references; maintain the + opaque-log/no-server-Loro architecture. + + Acceptance: + - Mid-download replacement, including at the same covered sequence, + cannot splice different checkpoints into accepted plaintext. + - A missing/invalid frontier never counts as contained. No pruning + covers unverified/unpersisted state; resumed work remains bounded. + - A lower-sequence new generation does not leave a legacy backup + labelled current. Restore uses ciphertext and client-held keys. + +ES-10 Encrypted registry fields and lifecycle proofs + + Role: Registry owner across TypeScript, Rust, and Swift. + Depends: ES-02, ES-03, ES-04, ES-05. RFC: Section 9; G4. + Seams: edge registry-core/registry-room; doc registry; sync registry; + engine workspace_host; iOS RegistryCore/RegistryClient. + + Deliver: + - Versioned field envelopes, original clocks, and independently + verifiable author contributions in server-merged rows. + - Authenticated field/row tombstones, creation/revival evidence, + GC/full-sync rules, and original-clock reseed behaviour. + - Separate wire state from decrypted projection and local overlays; + encrypt new application fields by default, not by a denylist. + - Reviewed rekey/generation path that does not manufacture newer + application clocks to force ciphertext replacement. + + Acceptance: + - Rename on A plus archive/status edit on B converges without a + whole-row overwrite. Null, absence, delete, revival, reseed, and + old clocks match the agreed existing semantics in all languages. + - Bad proofs cannot poison clocks/cursors or discard local state. + - Backend-visible IDs, fields, and clocks match the metadata contract; + titles, previews, paths, device names, and config are ciphertext. + +ES-11 Authenticated device channels and host-local attachments + + Role: RPC/engine owner and Swift relay owner. + Depends: ES-03, ES-04, ES-05, ES-06. RFC: Section 10. + Seams: rpc device_room/serve_connection; edge device-room; + engine uploads; iOS DeviceRelayClient/attachment transfer. + + Deliver: + - Reviewed end-to-end handshake, identity/profile binding, + directional session keys, replay protection, and rekey limits. + - No general RPC until peer verification. Encrypt method names, + arguments, replies, streams, file chunks, and error details. + - Keep outer routing opaque/minimal. Explicitly exclude private-key + and local key-management export from the forwarded RPC surface. + + Acceptance: + - Terminal, folder browser, file upload/read, and command flows work + through the relay without exposing their application payloads. + - Forged connId/echo, wrong profile, stale peer, and replay cannot + authenticate RPC. Test chunk retries and host reconnects. + - Existing path jails and command deduplication still pass; encryption + does not widen host filesystem or method permissions. + +ES-12 Close all secondary content paths and diagnostic leaks + + Role: Engine/blob owner with both frontend readers. + Depends: ES-09, ES-10, ES-11. RFC: Sections 3, 10, 13. + Seams: engine doc_host/diff_sync/sessions; edge blob/device sidecars; + native full-output/subagent/tail/diff readers. + + Deliver: + - Encrypt chat tails, working-tree diffs, device sidecar values, + R2 tool blobs, and finished subagent transcripts plus descriptors. + - Migrate/disable legacy diff publication for encrypted profiles; + no decryption-error fallback into legacy readers. + - Opaque object names, encrypted filenames/types/plaintext checksums, + ciphertext manifests, and atomic key-envelope publication. + - Structured safe diagnostics and an inventory test that detects + newly introduced cleartext publishers, including dormant paths. + + Acceptance: + - Every ES-02 surface has a canary test covering write, read, retry, + error, storage, and backup where applicable; decode encodings + before scanning. Base64 is not counted as encryption. + - Rich presence, future notification previews, and exported debug + data cannot bypass the encryption/explicit-consent boundary. + - Required product features still work; disabling a necessary path + is not a substitute for completing its encrypted implementation. + +ES-13 Cross-surface revocation and recovery-key continuity + + Role: Protocol/integration lead with security reviewer. + Depends: ES-05, ES-06, ES-08, ES-09, ES-10, ES-11, ES-12. + RFC: Sections 5-6, 11; G3. + + Deliver: + - Prepared -> envelopes durable -> old writers fenced -> new epoch + active transition, with CAS and crash/retry recovery. + - Fresh keys for subsequent chat, checkpoint, registry, blob, and + channel writes. Lazy rekey of dormant content on its next write. + - Catch-up policy before flushing an offline outbox; distinguish + accepted historical records from new old-epoch submissions. + - Recovery-kit replacement/compromise flow and continuously updated + encrypted recovery bundles across rotations. + + Acceptance: + - Removed device cannot read new-epoch content or make accepted new + writes under the tested honest-service/current-policy conditions. + - Concurrent rotation, mid-transition crash, and restored backups + leave retained devices and recovery able to obtain current keys. + - An offline retained phone sends its original intent exactly once + after rekey. Copied history is not claimed erased; stale-server + and partition limits are visible and match the RFC. + +ES-14 Migration, compatibility fences, and cleanup tooling + + Role: Engine/edge migration lead plus UI/platform reviewers. + Depends: ES-02, ES-07, ES-13. RFC: Section 12; G5-G6. + Seams: engine profile/store/startup; edge all legacy routes; native UI. + + Deliver as separate reviewable PRs: + A. Preflight/inventory and safe local schema/capability gates. + B. Old-writer quiescence, route/socket fences, and final revisions. + C. Encrypted generation copy, durable progress journal, verification, + activation, and restart/cancellation recovery. + D. Retention report and dry-run cleanup plan; separately authorised + deletion executor with scoped object lists and audit receipts. + + Acceptance: + - Every known legacy producer is upgraded/stopped before activation; + unavailable devices require the explicit confirmation flow. + Tests separate client no-plaintext behaviour from server rejection. + - Preserve lineage, chat/command IDs, host ledgers, and pending work. + Crash after each transition and resume without replay or loss. + - Missing/ambiguous sources block or require recorded user exclusion. + One profile's migration never blocks/decrypts another's data. + - Restore with the actual recovery kit before activation. Verify + actual deployment retention; no invented deletion deadline. + - Cleanup does not run on activation or claim erasure of unknown + copies. Post-activation rollback never enables plaintext sync. + +ES-15 Integrated reliability, leakage, performance, and security review + + Role: Integration/test lead and an independent security reviewer. + Depends: ES-07, ES-14. Harness scaffolding may begin during ES-02. + RFC: Section 14.2 and all closed protocol gates. + + Deliver: + - Repeatable synthetic topology: desktop host, second native client, + iOS peer, headless Linux host, and inspectable local/test edge. + - Fault injection for network, secure store, persistence, malformed + frames, stale/forked responses, rotation, and migration boundaries. + - Automated Rust/Swift compatibility fixtures and leakage scans of + requests, DO/R2 state, sidecars, errors, and diagnostic packages. + - Before/after performance report against ES-02 budgets; independent + protocol/implementation review with a tracked finding disposition. + + Acceptance: + - Every RFC acceptance row has a test/evidence link and accountable + reviewer. No unwaived baseline failure or unresolved release blocker. + - Restore works without the old devices or backend private keys. + Remove/rejoin, long offline sends, attachment/terminal operations, + and identical retry bytes pass across WS and HTTPS. + - Streaming/cold-open/resume/rekey budgets are met on target platforms; + memory/retries remain bounded. No per-update pairing round trips. + - Security reviewer confirms the published guarantees are supported; + limitations are explicit rather than quietly expanded promises. + +ES-16 Release controls, support, and staged activation + + Role: Release owner with maintainer/security sign-off. + Depends: ES-15. RFC: Sections 2, 4, 12-14. + + Deliver: + - Compatible edge and native build matrix; approved migration and + encrypted-backup restore runbooks; privacy-safe support checklist. + - Activation gate, safe pause controls, health indicators, and a + forward-fix/compatible-encrypted-rollback procedure. + - Staged progression from synthetic environments to explicit + internal opt-in, limited user opt-in, and broader opt-in. + - Clear product copy for full account-vault trust, recovery loss, + headless storage mode, local caches, and retained legacy copies. + + Acceptance: + - Maintainer approves each rollout expansion using ES-15 evidence + and current restore/migration health, not elapsed calendar time. + - Incident response can pause new activations or encrypted writes + without deleting keys, losing local edits, or enabling plaintext. + - Operators can diagnose failures without asking users for private + keys/recovery secrets; destructive recovery requires consent. + + +5. Coverage and Review Ownership +-------------------------------- + + Surface/control Implemented by Release evidence + -------------------------- ---------------------- ------------------ + Crypto formats/nonce rules ES-01, ES-03 Shared vectors + Device keys/unlock gate ES-04, ES-06, ES-07 Platform failures + Membership/bootstrap ES-05, ES-06 Pairing attacks + Chat rows and commands ES-08 Cursor/retry tests + Checkpoints/frontiers ES-09 Resume/restore + Registry fields/lifecycle ES-10 3-language merge + Device RPC/attachments ES-11 Peer/path/replay + Tails/diffs/device sidecars ES-12 Content canaries + R2/tool/subagent blobs ES-12 Blob/metadata scan + Epoch/key recovery ES-13 Rotation/restore + Legacy routes/local gates ES-02, ES-14 Migration matrix + Old cloud copies ES-14 Retention report + Logging/errors/support ES-12, ES-15, ES-16 Diagnostic scan + Product flows and rollout ES-07, ES-16 Native flow review + + Gate closure ownership: + + G1: ES-01 -> ES-03/ES-06/ES-11 evidence. + G2: ES-01 -> ES-03/ES-08/ES-09 evidence. + G3: ES-01 -> ES-05/ES-13 evidence. + G4: ES-02 -> ES-10 evidence. + G5: ES-02 -> ES-04/ES-14 evidence. + G6: ES-02 design -> ES-14 deployment and migration evidence. + + Require protocol/security review for ES-01/03/05/06/11/13 and migration + review for ES-14. Changes to shared wire/schema fixtures require Rust, + Swift, and edge review where applicable. No side may silently accept a + format the others cannot authenticate. + + +6. Verification Strategy and Existing Commands +----------------------------------------------- + + These are implementation verification entry points, not commands run + while writing the plan. Audit/install prerequisites first. Start with + targeted tests, then run broader regressions before milestone closure. + Record pre-existing failures; do not relax controls or absorb unrelated + repairs into encryption work to obtain a green result. + + From the repository root, relevant existing Rust package names are: + + cargo test -p zeron-doc -p zeron-sync -p zeron-rpc + cargo test -p zeron-engine + cargo test -p zeron-proto -p zeron-ui + cargo fmt --all -- --check + + Add the crypto module/crate's actual test command when ES-03 chooses its + location. Test both macOS and Linux build paths; a macOS-only build does + not validate headless credential storage or restart behaviour. + + Existing edge entry points, from the repository root: + + npm --prefix edge run typecheck + npm --prefix edge run test:unit + npm --prefix edge run test:workerd + + The shared Zeron Xcode scheme includes ZeronTests. On a machine with + the supported SDK and an installed iOS simulator, from apps/ios: + + xcodebuild -project Zeron.xcodeproj -scheme Zeron \ + -destination 'platform=iOS Simulator,id=' test + + Substitute a real local simulator ID; it is not a literal command + argument. Secure-store/background lifecycle tests also need supported + physical-device coverage, not only simulated storage adapters. + + Extend existing tests in: + + crates/sync/src/chat_client/tests.rs + crates/sync/tests/ + crates/engine/tests/ + crates/rpc/tests/device_room.rs + apps/ios/ZeronTests/ + edge/src/*test.ts and edge workerd test configuration + + Build a dedicated encrypted-sync integration fixture rather than using + production rooms, real repositories, or user secrets. Shared canaries + should exercise source text, commands, diffs, titles, branch/path names, + binary attachments, and subagent output. Include success AND error + paths. Scan decoded wire/storage representations as well as raw bytes. + + Performance baselines must use the same corpus/device/link settings + before and after encryption. Record first usable sidebar, cold chat + open, streaming latency, attachment/terminal throughput, Range resume, + rekey cost, ciphertext overhead, peak memory, and retries. Approve + numeric budgets in ES-02 rather than inventing them in this plan. + + +7. Risk Register and Stop Conditions +------------------------------------ + + Risk Mitigation / accountable package + --------------------------- ----------------------------------------- + Crypto/library mismatch Freeze one format and negative vectors; + vetted implementations. ES-01/03. + + Nonce reuse after restore Crash/clone-safe allocation and usage + bounds; no improvised counters. ES-01/03. + + Permanent key loss Durable device/recovery envelopes before + activation; actual restore drills. ES-06/13. + + Cursor advances past error Result-bearing verified apply, atomic + persistence, own-ACK gap tests. ES-08/09. + + Registry conflicts regress Per-field proofs, lifecycle evidence, + original-clock rekey/reseed. ES-02/10. + + Secondary plaintext leak Default-encrypted typed boundaries and + full publisher/error inventory. ES-12/15. + + Old clients disclose data Upgrade/stop preflight plus client gates; + server rejection alone is not enough. + Ownership design and tests. ES-02/14. + + Rotation strands peers Staged publication/CAS, offline catch-up, + historical-key retention. ES-05/13. + + Migration re-executes work Preserve semantic IDs, lineage, and host + processed ledgers; fault injection. ES-14. + + Misleading deletion claim Measured retention and consented dry-run + cleanup; report unknown copies. ES-14/16. + + Hidden platform gap Rust/Swift parity and macOS/Linux/iOS + lifecycle coverage before opt-in. ES-15. + + Scope creep Per-user vault only. New sharing/runner + restrictions require a separate design. + + Stop a package when a critical protocol choice is still unspecified, + a new plaintext path is unowned, an invariant needs weakening, or a + schema cannot preserve existing semantics. Escalate that decision; + do not work around it with fallback, silent drops, or broader trust. + + Stop rollout for unexplained decryption/cursor errors, key-loss cases, + plaintext canary leakage, incomplete migrations, replayed commands, or + a failed recovery drill. Use safe pause/forward-fix procedures. Never + "recover availability" by accepting plaintext or resetting trust. + + +8. Release Checklist and Immediate Next Action +----------------------------------------------- + + [ ] RFC G1-G6 decisions and acceptance evidence reviewed. + [ ] ES-01 through ES-15 complete; required review findings resolved. + [ ] Every content/metadata path accounted for and tested. + [ ] Desktop, iOS, and headless supported-version matrix approved. + [ ] Recovery after multiple rotations and from backup demonstrated. + [ ] Legacy upgrade/stop and supported-downgrade flows verified. + [ ] Migration crash matrix and command deduplication verified. + [ ] Old-copy retention report and cleanup consent flow available. + [ ] Latency/memory/byte budgets met without weakened protection. + [ ] Safe pause, compatible rollback, and support procedures rehearsed. + [ ] Maintainer authorises the next rollout stage (ES-16). + + Immediate next action is ES-01 and ES-02 review/discovery, not wrapping + ChatClient bytes in isolation. Close the protocol and ownership/schema + questions, record the exact decisions, and then begin ES-03 fixtures. + + Suggested PR grouping: + + - One logical PR per ES package where review stays manageable. + - Split ES-03 into reviewed fixture/codec changes per language. + - Split ES-08 into durable cursor/outbox tests, Rust integration, + and Swift integration, without exposing the activation switch. + - Split ES-14 along A-D above. No cleanup execution in a migration PR. + - Keep all split parts tied to the parent package's acceptance gate; + merged preparatory PRs do not mean that package is complete. + + Update this plan with actual issue/PR/evidence references as work lands. + Do not invent issue links, assign people without agreement, or mark + implementation complete based only on this planning document. + + +9. Initial Discovery and First Review Slice (2026-09-04) +------------------------------------------------------- + + RFC Draft 00 was committed separately as 812e64c on feat/encrypted-sync. + This changes no production code; the inspected code baseline remains + a0650ab. This plan is not included in that commit. Existing iOS project, + scheme, and workspace-user changes were left untouched. + + This is preliminary ES-01/ES-02 discovery, not package acceptance. + G1-G6 remain OPEN. Owners/reviewers are not yet assigned. No dependency + installation, implementation test run, deployment inspection, or + migration has been performed during this discovery pass. + +9.1. Confirmed integration constraints + + A. Profile and route ownership (ES-02/04/05/14). + EngineProfile::account_scoped in crates/engine/src/profile.rs stores + data under org/user; local profiles use a separate root. In contrast, + edge/src/index.ts routes chat2 by chat ID and device rooms by device + ID, forwarding authenticated user identity without an org-scoped + room name. Tool blobs use blob/{user}/{chat}/{part}. Registry and + legacy workspace routes explicitly scope rooms by org AND user. + Define authoritative legacy-object-to-profile mappings before + implementing per-profile fences. Do not infer ownership from a new + caller-provided org field or block every profile of the same user. + + B. Durable receive outcomes (ES-08). + ChatDocSink::apply_row and advance_cursor return void. EngineChatSink + logs failed imports and still attempts cursor persistence; + persist_with_cursor logs storage errors without returning failure. + apply_checkpoint can therefore return success after persistence + failed. Swift SessionStore likewise advances its cursor after failed + row import and schedules saving with saver.poke(). + Require explicit verified/durable results in BOTH languages before + integrating encrypted records. Test failed imports, failed saves, + causal pending state, and own ACKs interleaved with unread rows. + + C. Immutable checkpoint identity (ES-01/09). + EdgeCheckpointFetcher::fetch in crates/engine/src/chat2_host.rs uses + x-chat2-checkpoint-seq as its replacement validator. A replacement + at the same covered sequence needs a distinct immutable revision. + Define manifest/revision binding before changing Range resume; test + a replacement between partial responses with an unchanged sequence. + + D. Registry semantics (ES-02/10). + edge/src/registry-core.ts applies strictly newer per-field clocks; + update cannot create/revive a row, while upsert can. Top-level null + in an op's set deletes a field. Row deletion clears fields/clocks, + and rowToSeedOp preserves original clocks for live values. + Specify authenticated field tombstones and creation/revival evidence + independently of encrypted values. Preserve current null/absence + semantics; adding literal-null assignment would be a schema change. + Rekey must not manufacture a newer application clock. + + E. Command-bearing relay (ES-01/11). + crates/rpc/src/device_room.rs dispatches RPC payload text into a + virtual serve_connection stream keyed by the relay's from field. + That routing ID is not a device-key possession proof. Insert the + reviewed peer/channel handshake before creating an application RPC + stream, and bind it to a single profile. Preserve existing method + authorization and attachment path restrictions after decryption. + + F. Independent plaintext publishers (ES-12). + DocHost::chat2_maintenance publishes transcript JSON to /chat2/*/tail. + diff_sync publishes checkout paths, branches, and patches to legacy + /diff/*. DocHost::upload_tool_sidecar uploads output/diff bytes to + /blob/*; fetch_tool_blob reads the body as text. These bypass chat + row transport. Give each a typed encrypted writer AND reader, plus + retry/error canaries. HTTP metadata and blob identifiers also need + review; wrapping the Loro stream does not cover them. + + G. Secure storage (ES-02/04). + The Swift Keychain.save helper in Auth/AuthClient.swift deletes the + old item before adding a replacement, ignores return statuses, and + uses AfterFirstUnlock rather than an explicit device-only attribute. + Do not reuse it unchanged for enrollment. Decide checked persistence, + accessibility, restore/clone behavior, and Linux credential modes + before declaring a device approved. Auth refresh tokens and vault + private keys remain separate credentials. + +9.2. Next review deliverables + + First, prepare two bounded design submissions under M0: + + ES-01 review: a Rust/Swift library compatibility and provenance + matrix; one proposed pairing/RPC channel construction; exact record + encoding and signing/AAD inputs; nonce/clone-safety and usage limits; + membership/epoch closure and recovery transitions. Each choice needs + a rationale, negative fixture specification, and reviewer decision. + The RFC candidate suite is a starting point, not an approved codec. + + ES-02 review: expand 9.1 into the complete route/publisher/reader/ + storage matrix; propose registry lifecycle proofs and profile + ownership/fencing rules; specify secure-store and downgrade policy; + capture audited local test and synthetic performance baselines. + Include sidecars, subagents, attachments, diagnostics, backups, and + retired routes. Deployed retention stays UNKNOWN until separately + approved inspection; repository source cannot establish deletion. + + After those gates are reviewed, the first implementation submission is + ES-03: bounded record codecs and shared Rust/Swift positive/negative + fixtures, with no transport activation. ES-04/05 then establish durable + device keys, the fail-closed vault lifecycle, and signed bootstrap. + Chat integration follows those foundations; ES-08's first narrow slice + is failing durability/cursor tests and explicit apply outcomes, followed + by immutable encrypted outbox integration across WS and HTTPS. + + The existing M0-M5 sequence remains unchanged. Neither these discovery + notes nor committing the RFC closes a protocol gate or enables the + encrypted-sync switch. + + +10. Decision Proposals and Capability Evidence (2026-09-04) +---------------------------------------------------------- + + Status: Q1-Q4 option A ACCEPTED by the maintainer on 2026-09-04. + Native CryptoKit plus a shared Rust channel, device-bound background- + accessible iOS keys, explicit unattended runner setup, and a grouped + checksummed text recovery kit are the selected directions. Alternatives + below remain for decision history. Section 10.4 is not a frozen protocol; + acceptance does not waive dependency vetting or close G1-G6. + +10.1. Capability probe completed + + Added scripts/check-crypto-capabilities.swift. It uses only Apple's + installed CryptoKit and Foundation, creates synthetic keys in memory, + performs no network/keychain operations, and prints no key material. + + Verified with Apple Swift 6.3.3: + + swift scripts/check-crypto-capabilities.swift + + Result: 14 checks passed on macOS: AES-256-GCM round trip and rejection + of changed AAD/key/ciphertext; Ed25519 verification and changed-message/ + wrong-signer rejection; HKDF-SHA256 repeatability and label separation; + HPKE X25519/HKDF-SHA256/AES-256-GCM round trip and rejection of changed + AAD/info/recipient/ciphertext. + + xcrun swiftc -typecheck -target arm64-apple-ios26.0-simulator \ + -sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \ + scripts/check-crypto-capabilities.swift + + Result: passed with the installed iOS Simulator 26.5 SDK. This is an + iOS compile check, NOT an iOS runtime test. The app targets iOS 26; + Apple's HPKE API is available from iOS 17. The candidate suite needs + HPKE.Ciphersuite(kem:kdf:aead:), not a matching named static constant. + + An initial probe failure was a Data indexing error in the test script: + CryptoKit's ciphertext can have a nonzero startIndex. Mutation now uses + startIndex; the interpreter run passed after that correction. + + This probe does not define a wire format, enrollment handshake, nonce + allocator, key-storage policy, or Rust/Swift interoperability. It is + discovery evidence only and is not linked into an application target. + Existing Rust/edge/Xcode suites have not run in this discovery pass; + edge/node_modules is absent. No third-party packages were installed. + +10.2. Library shortlist and review limits + + Candidate Verified metadata / remaining review + ------------------ -------------------------------------------------- + Apple CryptoKit Built-in platform APIs support the candidate suite; + local positive/negative capability probe passed. + + snow 0.10.0 crates.io: published 2025-07-19, publisher mcginty, + not yanked, MIT OR Apache-2.0, MSRV 1.85. + 1Password reports a Trail of Bits assessment and + remediation work. RustSec RUSTSEC-2024-0011 is + patched from 0.9.5. Audit scope and every finding's + correspondence to the selected release still need + verification; this is not full audit clearance. + + hpke 0.13.0 crates.io: published 2025-02-19, publisher rozbb, + not yanked, MIT/Apache-2.0. Supports the candidate + suite. Upstream explicitly says no formal audit; + Cloudflare's cited review covered 0.8, not 0.13. + + Both registry responses have trustpub_data=null; neither response + supplies a trusted-publishing attestation. This alone proves neither + maliciousness nor absence of other provenance. Registry publication + times above take precedence over differing dates shown by docs.rs. + Requests used a generic tool/version User-Agent, no personal contact. + + These are candidates, not selected dependencies. Before cargo add or + execution: verify exact archives/checksums against publisher/source, + review build scripts and selected transitive dependencies, scan actual + code for process execution/network/secret-path/unsafe behavior, check + current advisories and audit fixes, and record the result. Minimize + enabled algorithms/features and pin reviewed versions. Do not equate + version age, a README, or an audit of another version with source review. + + Swift channel research found a production-use warning in + swift-libp2p/swift-noise and an older swift-noise-protocol release. + Neither is a recommended production dependency on this evidence. + Do not port the Noise handshake by hand to avoid a Rust bridge. + +10.3. Options requiring maintainer choice + + Q1. Native integration boundary (G1; ES-03/06/11). + A [recommended]: CryptoKit for Swift content/key-envelope operations, + a small reusable Rust crypto crate for Rust callers, and a shared + Rust Noise channel behind a narrow Swift bridge. Native keychain + adapters remain outside cryptographic primitives. Shared fixtures + test the independently implemented record boundary in both languages. + This follows the app's native-client design without maintaining two + independent handshake implementations. It adds an iOS Rust artifact + build/signing/packaging path that the repo does not currently own. + B: share the entire Rust security core with Swift. Less duplicated + protocol logic, but a larger foreign-function interface and less + use of Apple's native crypto APIs. Keep independent CryptoKit/RFC + vectors as an oracle; two calls to one core are not independent + cross-implementation evidence. + + Q2. Apple key accessibility (G5; ES-04). + A [recommended]: non-syncing, device-bound keys. On iOS, use + AfterFirstUnlockThisDeviceOnly to permit background work after the + first unlock following boot. Restoring to a different phone requires + approval/recovery. Handle keychain persistence errors explicitly. + B: WhenUnlockedThisDeviceOnly; pause key-dependent work whenever the + phone is locked. Stricter access, but background sync is restricted. + macOS needs its own checked login/data-protection keychain policy; + do not assume iOS accessibility behavior proves macOS lock behavior. + Neither option claims to encrypt existing plaintext local caches. + + Q3. Default headless credential mode (G5; ES-04/07). + A [recommended for a runner]: explicitly opt into unattended mode + at pairing, using systemd encrypted credentials where supported. + Use the provisioned secret to protect a mutable local keyring; do + not try to rewrite systemd's immutable runtime credential on rekey. + Root/VPS snapshots remain trusted. If secure storage is unavailable, + stay locked and ask for a supported provisioning choice. + B: unlock on each boot by default; safer unattended exposure but + jobs cannot resume encrypted sync until an operator intervenes. + Both modes remain available. No silent plaintext-file fallback. + + Q4. Recovery-kit representation (ES-06/07). + A [recommended]: grouped, checksummed Base32 encoding of the random + 256-bit recovery secret, plus vault/trust-anchor metadata in a + locally saved recovery file. Confirm possession before activation. + Straightforward password-manager storage, no language-specific list. + B: a reviewed mnemonic encoding plus the same metadata; easier to + transcribe verbally but adds word-list, normalization, and checksum + compatibility work. Do not invent a new mnemonic scheme. + Neither choice changes the no-email/no-account-reset recovery rule. + +10.4. Technical defaults proposed for review, not user crypto design + + G1: retain the RFC's AES-256-GCM/HKDF-SHA256/Ed25519 and X25519 HPKE + candidate suite. Prefer one fixed Noise_XX_25519_AESGCM_SHA256 + channel profile for pairing and device RPC, using a vetted release + of snow in both clients. Enrollment authenticates the exchanged + identity via endpoint-generated QR/comparison; established peers + verify signed membership and expected static keys before RPC. + Exact transcript binding, human-verification strength/attempt + limits, channel key separation, and framing require reviewed vectors. + XX alone does not make a server-provided public key trustworthy. + + G2: prefer an RFC 8949 deterministic-CBOR profile for signed records, + with a deliberately narrow schema (integer keys, byte strings, + fixed field sets; no floats, tags, indefinite lengths, duplicates, + or unknown fields for a fixed version). JSON may carry opaque + Base64 bytes, never determine signature meaning. Alternative: + bespoke fixed-layout binary records; fewer library dependencies, + but more project-specific encoding/extension rules to maintain. + Specify signature/AAD domains, exact bytes, bounded parsing, and + nonce/key-use limits before adding codecs. Persist sealed outbox + bytes once; no process-local counter as the only nonce safeguard. + A crash/clone-safe nonce construction remains an explicit G2 task, + not a decision delegated to an API's random-nonce default. + + G3: a signed, parent-linked membership history with compare-and-swap; + prepare/durable-envelopes/fence/activate epoch transitions. Pin + known revisions locally and retain historical read keys. Specify + signed accepted-history closure manifests and bounded membership + checkpoints before accepting old-epoch history after revocation. + Do not treat a server sequence or writer timestamp as proof that + an old-epoch write preceded revocation. Global freshness under a + malicious partition remains outside the RFC's guarantee. + + G4: retain per-field LWW semantics with independently signed encrypted + field values and explicit lifecycle proofs. Prefer a signed new + registry generation for rekey, preserving semantic row IDs and + original clocks, over rewriting clocks or adding a second LWW race. + Define deletion/revival/GC and cutover concurrency in three-language + fixtures before implementing the encrypted registry schema. + + G5: follow Q2/Q3; enrollment completes only after checked durable key + storage. Device restore must not silently clone writer identity. + Account switching, sign-out, and secure-store lock state are + separate from transport state. Safe local-layout/downgrade behavior + still needs a concrete versioned design and failure tests. + + G6: introduce a profile-scoped vault control Durable Object and explicit + versioned ciphertext routes/generations. Maintain authoritative + legacy ownership mappings, not caller-inferred organization labels. + Gate producers before serialization and fence every legacy route + and existing socket. Migration is explicit copy/verify/activate, + with no plaintext dual-write and no automatic legacy deletion. + Actual deployed retention remains UNKNOWN pending approved access. + + These defaults narrow the work but do not close the gates. In particular, + G2 nonce allocation, G3 accepted-history closure, G4 lifecycle proofs, + and G6 legacy ownership/fencing still require exact designs and review. + +10.5. First bounded implementation after decisions + + Freeze the accepted architecture and reviewed record schema in the RFC, + completing the relevant ES-01/02 decisions before marking ES-03 ready. + Then introduce the reusable Rust crypto boundary and Swift adapter/ + bridge chosen in Q1, with no networking, application activation, or + key export RPC. Add shared synthetic positive/negative fixtures first. + Complete archive/source review before installing any dependency. + Native secure storage and vault-state gating follow; chat integration + cannot substitute for those foundations. No additional commit or push + is implied by starting this work. + + Sources consulted (2026-09-04): + https://developer.apple.com/documentation/cryptokit/hpke/ciphersuite.md + https://developer.apple.com/documentation/security/ksecattraccessibleafterfirstunlockthisdeviceonly.md + https://docs.rs/crate/hpke/0.13.0/source/README.md + https://crates.io/api/v1/crates/hpke/0.13.0 + https://crates.io/api/v1/crates/snow/0.10.0 + https://1password.com/blog/strengthening-snow-for-the-open-source-community + https://rustsec.org/advisories/RUSTSEC-2024-0011 + https://noiseprotocol.org/noise.html + https://www.rfc-editor.org/rfc/rfc8949.html + https://systemd.io/CREDENTIALS/ + + +11. First Isolated Primitive Slice (2026-09-04) +----------------------------------------------- + + Q1-Q4 option A is accepted and recorded in RFC Section 15. M0 and the + exact protocol gates remain open. Standards-only adapters and fixture + scaffolding have begun; ES-03 as a complete record-codec package is NOT + accepted. No signed-record encoding or nonce allocator was invented to + get ahead of those gates. + +11.1. Implemented boundary + + Added crates/crypto (zeron-crypto) and the native Swift counterpart + apps/ios/Zeron/Sync/VaultCrypto.swift: + + - AES-256-GCM opening with a 12-byte nonce and 16-byte appended tag. + Validate key/nonce/tag lengths and caller-supplied plaintext size + budget before allocating a decryption buffer. Return no plaintext + on authentication failure; do not mutate the caller's ciphertext. + - Ed25519 signature verification with explicit key/signature lengths. + This is a primitive, not membership validation or RPC authorization. + - HKDF-SHA256 extract/expand with output restricted to 1..8160 bytes + before allocation. This is a key-producing adapter; zero-byte keys + are rejected even though HKDF itself can describe empty output. + - Coarse typed errors without key/payload contents. Rust SecretBytes + redacts Debug output, is not Clone/Copy/Serialize, and uses a + zeroizing allocation for output and decryption-failure cleanup. + Swift retains derived keys as SymmetricKey. No claim of perfect + process-memory erasure, including backend internal key schedules. + + The caller still must bound framing/AAD/message inputs and authenticate + profile, membership, object, kind, epoch, and canonical record bytes. + These primitives do not supply those checks. No application calls the + adapters yet. There is deliberately no production seal/sign API while + nonce allocation, signed record domains, and durable outbox rules are + open. Raw sealing/signing occurs only in synthetic fixture tests. + +11.2. Dependency scope and supply-chain check + + Reused ring 0.17.14 and zeroize 1.9.0 already in the root lockfile and + local registry cache. Both are exact-pinned with only alloc requested; + no existing dependency versions were changed and no new package was + downloaded. HPKE and Noise packages were not added. + + Registry metadata checked with a generic tool User-Agent: + + ring 0.17.14: published 2025-03-11 by briansmith, not yanked, + no trusted-publishing metadata. Registry checksum matches Cargo.lock. + RustSec's AES overflow issue is patched from 0.17.12; the legacy + unmaintained advisory concerns versions before 0.17. + + zeroize 1.9.0: published 2026-06-12, not yanked, trusted-publishing + metadata names RustCrypto/utils and revision + 0b715735a660a8566ccd240bf42489fe2ed98efb. Registry checksum matches + Cargo.lock. No build script or default derive/serialization feature. + + Inspected cached manifests, ring's native-build dispatch and compilation + paths, AES-GCM/HKDF/Ed25519 entry points, and Zeroizing/Vec cleanup. + Source scans found no listed credential paths or unexpected network + calls. ring's process-spawn matches are native build tooling (compiler, + assembler/Perl generation paths), not application networking; packaged + builds select pregenerated assembly. zeroize uses expected volatile + writes/optimization barriers. This is a scoped supply-chain review, not + an independent cryptographic audit or complete transitive-code audit. + +11.3. Shared fixtures and findings + + crates/crypto/tests/fixtures/primitives.json contains public standard + test keys only: AES-256-GCM empty/block vectors, RFC 8032 Ed25519 vectors, + RFC 5869 HKDF, and a synthetic binary/nonempty-AAD fixture generated by + CryptoKit and checked independently by ring. + + Rust tests and scripts/check-crypto-capabilities.swift consume this + same file. Both AES implementations produce the expected bytes. Both + signature verifiers accept the published signatures, and Rust signing + matches RFC 8032. The script can emit additional CryptoKit signatures; + Rust requires and verifies them when ZERON_CRYPTO_TEST_VECTORS is set. + + Important discovery: CryptoKit signing on the tested platform does not + produce the deterministic RFC signature bytes. Tests therefore assert + verification interoperability, NOT identical output from two signing + calls. An initial exact-signature assertion failed and was replaced + with fresh CryptoKit-signature verification by Rust. Canonical signed + input and immutable stored envelope bytes remain required; retries must + not recreate signatures and then assume identical envelopes. + + Negative tests cover key/nonce/tag/signature lengths, changed bytes, + AAD/key/nonce substitution, output limits, nonzero Data slice indices, + unchanged input on decryption failure, and redacted Rust secret Debug. + This is not yet an adversarial Ed25519 key-acceptance/point-encoding + corpus. Strict verification compatibility and identity-key admission + rules remain part of G1/G2 review before trust-record use. + +11.4. Verification and reproducible entry points + + Normal Rust entry points once the workspace's pinned Git sources exist: + + cargo test -p zeron-crypto --locked --offline + cargo clippy -p zeron-crypto --locked --offline --all-targets -- -D warnings + cargo fmt -p zeron-crypto -- --check + + This checkout cannot resolve the full workspace offline: GPUI's pinned + e2ddcc6805f8c5088e62a60dfe517abcccd61a9a revision is not checked out. + No fetch or unrelated dependency change was performed to work around it. + Verification used an ignored target/crypto-verification manifest whose + lib path points at the ACTUAL crates/crypto/src/lib.rs, with the same + direct dependencies and locked serde/serde_json versions. cc was pinned + back to the root's 1.2.67 after isolated resolution selected cached 1.4.0. + Final tests and Clippy passed with the root's cc version. The new local + package stanza was copied from Cargo's isolated generated lockfile into + the root lockfile; existing package records remain unchanged. + + cargo test --manifest-path target/crypto-verification/Cargo.toml --offline --locked + cargo clippy --manifest-path target/crypto-verification/Cargo.toml --offline --locked --all-targets -- -D warnings + + Result: 7 Rust test groups passed; Clippy passed with warnings denied. + Workspace metadata and package-specific rustfmt checks also passed. + This is not a full workspace build or test result. + + Historical Swift/shared-vector command for this slice (superseded by + the normal-workspace commands in Section 15): + + swiftc apps/ios/Zeron/Sync/VaultCrypto.swift \ + apps/ios/Zeron/Sync/VaultSignedRecord.swift \ + scripts/check-crypto-capabilities.swift \ + -o target/crypto-capabilities-probe + ./target/crypto-capabilities-probe \ + crates/crypto/tests/fixtures/primitives.json \ + target/crypto-swift-vectors.json + ZERON_CRYPTO_TEST_VECTORS="$PWD/target/crypto-swift-vectors.json" \ + cargo test --manifest-path target/crypto-verification/Cargo.toml --offline --locked + + Result: Swift shared vectors and HPKE platform probe passed; Rust passed + with the fresh Swift signatures. The emitted file holds public test-key + fixtures only. HPKE cross-language support is not implemented/tested yet. + + xcrun swiftc -typecheck -target arm64-apple-ios26.0-simulator \ + -sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \ + apps/ios/Zeron/Sync/VaultCrypto.swift \ + apps/ios/Zeron/Sync/VaultSignedRecord.swift \ + scripts/check-crypto-capabilities.swift + + Result: iOS simulator-target compile check passed. Additionally, the + four new VaultCryptoTests XCTest methods passed using a temporary native + macOS runner against the actual Swift adapter source. The runner needed + Xcode's Swift XCTest overlay and private-framework search paths. This + does not substitute for the full iOS app test target or physical-device + secure-store tests. The XCTest source is in the existing synchronized + test group; no existing Xcode project/scheme changes were modified. + + Next: finish G1/G2's exact record/identity verification and nonce design, + vet HPKE/Noise and codec dependencies, then extend these fixtures to + signed envelopes and cross-language HPKE. Secure storage, the channel + bridge, pairing/recovery, all transport integrations, migration, and + activation remain pending. No commit, push, or deployment performed. + + +12. Signed-Wrapper Slice and Pending Nonce Decision (2026-09-04) +--------------------------------------------------------------- + + RFC Section 7.5 fixes the candidate wrapper grammar for conformance + work: an exact deterministic-CBOR map, domain-separated Ed25519 input, + opaque IDs, epoch/membership binding, and a bounded opaque payload. + This is a signed wrapper, not yet an encrypted-content envelope. + The decoder must keep unverified and verified payload states distinct. + Membership authorization, freshness, and inner-payload semantics are + not inferred from a syntactically valid or correctly signed wrapper. + + There is no CBOR dependency in the current crypto crate; none was found + in the inspected active registry cache. This slice uses a non-recursive + implementation of the fixed map grammar (unsigned integers, byte strings, + fixed ordered keys), not a general CBOR decoder or new cryptographic + primitives. Cross-language canonical-byte and malformed-input fixtures + are required. A future replacement library must pass the same strict + acceptance corpus. + + Proposed G2 nonce direction, still awaiting decision/review: + + A [recommended for investigation]: derive a fresh AES content key + per immutable record using HKDF and a fresh random 256-bit salt. + Bind derivation/AAD to the version, suite, vault, generation, epoch, + content-key ID, purpose, author, and immutable revision. One AEAD + invocation per derived key permits a fixed nonce for that key. + Persist the entire signed ciphertext once; a retry never seals again. + Rekey/re-encryption requires a fresh salt and transport revision. + + This avoids coupling nonce safety to a restorable SQLite counter. + It does not create an absolute uniqueness guarantee: entropy quality, + salt/derived-key collision bounds, aggregate usage limits, and exact + domain encodings still require review. For illustration, at most + 2^32 independently random 256-bit salts have collision probability + below 2^-193; this is not a complete AES-GCM security bound or an + approved operational budget. Restoring a disk backup must use fresh + OS entropy/new identity policy; arbitrary live VM/RNG-state cloning + cannot be declared safe merely by writing "random" in the design. + + B: per-writer keys and durably reserved nonce-counter ranges. This + uses conventional counter nonces but needs atomic allocation across + processes and a restore/clone barrier outside the restorable outbox. + A database transaction alone cannot prevent nonce reuse after backup + rollback. More storage/platform state must become part of the proof. + + Neither construction is implemented or approved by this section. The + signed wrapper deliberately leaves the inner encrypted payload format + open so that its parser/signature work cannot accidentally commit us + to an unreviewed nonce allocator. No production seal API is added. + +12.1. Implemented wrapper and verification evidence + + Added crates/crypto/src/record.rs and the native counterpart + apps/ios/Zeron/Sync/VaultSignedRecord.swift. Parsing exposes only named + untrusted routing/binding metadata. Context and signature verification + are required to obtain the payload-bearing verified-record type. Rust + borrows the input payload; Swift retains bounded Data slices. Verified + fields cannot be mutated through these APIs, and record Debug output is + redacted. Neither type decides membership validity or payload meaning. + + Added six Rust record test groups and four additional XCTest cases. + Shared fixtures now include exact canonical signing bytes, a Rust-made + reference signature, and 21 malformed CBOR mutations consumed by both + languages. Swift emits a fresh signed wrapper; Rust checks its exact + encoding and verifies it against the expected binding and trusted key. + + Coverage includes every binding field, every single-byte mutation of + the sample record, every truncation, trailing bytes, unknown versions/ + kinds, duplicate/reordered keys, forbidden CBOR types, oversized declared + lengths, zero epoch, size-limit arithmetic, redaction, nonzero Data slice + indices, all unsigned integer widths through UInt64/u64 maximum, and + byte-string length boundaries through 65536 bytes. This is conformance + coverage, not a substitute for fuzzing or independent protocol review. + + Verification on 2026-09-04: + - 13 Rust tests passed through the existing isolated manifest, + including fresh Swift signatures AND complete signed-wrapper bytes. + - 8 native XCTest cases passed against the actual Swift sources. + - Swift shared-fixture runner passed; iOS simulator-target typecheck + passed with both Swift adapter files included. + - Clippy passed with warnings denied; package rustfmt passed. + - A negative Swift compile probe confirmed that unverified payload + access and direct construction of a verified record are rejected + by the compiler's private/fileprivate access checks. + + Full workspace testing retains the earlier missing-GPUI limitation; + these are isolated source tests, not a full app/platform release result. + No dependency, transport, account state, existing Xcode project/scheme, + or activation behavior was changed by this slice. No commit or push. + + G1-G3 remain open. Before using these records for authorization, finish + strict key-admission/verification compatibility, trusted membership + lookup, genesis/epoch-transition schemas, and replay/history rules. + Before encrypting content, resolve the nonce proposal and exact inner + payload/AEAD/key-binding format. No signature-only record may be used as + evidence that workspace content is end-to-end encrypted. + + +13. Ed25519 Encoding Hardening (2026-09-04) +------------------------------------------ + + PR #252 was open with review still required when this work began. The + automated review had reported no issues; that is not protocol approval. + The subsequent standing direction in Section 2 authorises committing + and pushing this and later notable milestones to the same PR. + +13.1. Reproduction and scope + + Both ring and CryptoKit accepted A = R = the encoded identity point, + S = 0 for synthetic messages. With that point supplied as the trusted + public key, the signed-wrapper adapters also returned a verified record. + Added failing regression tests at BOTH the primitive and wrapper layers + before changing verification; two Rust and two XCTest cases failed as + expected. This is not a signature break under a correctly generated + public key. It demonstrates why a raw signature check is insufficient + as device-key possession/admission evidence for arbitrary input keys. + + No live application transport currently calls these adapters, so this + finding does not establish an exploit against deployed sync. It is an + important restriction to enforce before implementing enrollment. + +13.2. Added restrictions + + RFC Section 7.6 records the exact candidate byte-level policy: + + - Inspect A and R as 32-byte compressed Edwards encodings. Reject + y >= 2^255-19 and the small-order y coordinates, for either sign. + - Require the full 32-byte scalar S to be less than the Ed25519 + subgroup order L; do not mask high bits or reduce modulo L. + - Preserve the original input bytes for the library's signature + calculation. Sign-bit masking is performed only on a local copy + used for the public-coordinate check. + + Rust and Swift apply the same prechecks before their existing signature + libraries. Failures remain AuthenticationFailed / authenticationFailed; + signed wrappers map them to InvalidSignature / invalidSignature. No + alternate verifier, fallback, curve arithmetic, or signature algorithm + was introduced. Helpers are explicitly named encoding PRECHECKS and + are not a device-admission API. + + These restrictions intentionally do not claim complete prime-subgroup + admission or identical behavior for all mixed-order point constructions. + Passing the precheck is necessary, not sufficient, for signature success + or key acceptance. Full G1/G2 verification/admission review remains open. + +13.3. Fixtures, evidence, and sources + + Expanded the shared fixture file with 13 point-encoding cases, 9 scalar + cases, and 8 complete signature rejection cases. The Rust and Swift + harnesses also test both point sign bits, the entire noncanonical field + range p through 2^255-1, S boundaries including L-1/L/L+1 and high bits, + malformed lengths, immutable input, and nonzero Data slice indices. + Existing positive RFC and fresh CryptoKit-signature fixtures still pass. + Explicitly positive encoding-only cases prevent tests from confusing + an encoding check with successful curve-point admission. + + Verification: + - 17 Rust tests passed against the actual source through the existing + isolated manifest, including fresh Swift signatures/records. + - 11 native XCTest cases passed through the macOS source runner. + - Shared Swift fixtures and iOS simulator-target typecheck passed. + - Clippy passed with warnings denied; no new dependency was added. + + Inspected RFC 8032 and the small-order/canonical-encoding reference in + libsodium 1.0.18, source blob ec6d86f5ec5243cc01a951ba2f13cccc574009a5: + https://www.rfc-editor.org/rfc/rfc8032.html + https://github.com/jedisct1/libsodium/blob/1.0.18/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c + https://hdevalence.ca/blog/2020-10-04-its-25519am/ + + Only the public mathematical coordinate/order values informed these + byte checks. No libsodium dependency was installed or executed, and + this does not claim compatibility with every libsodium validation rule. + The validation-criteria analysis explicitly explains why a small-order + exclusion list alone is not a full solution to mixed-order agreement. + + The nonce proposal in Section 12 is still undecided. No encrypted write, + key store, enrollment flow, migration, activation, or existing Xcode + project/scheme behavior changed. Full workspace verification retains the + earlier missing-GPUI limitation; full iOS device and security review are + still required. Next work remains complete key-admission policy and + reviewed HPKE interoperability before wiring key-management transports. + + +14. Workspace Verification Restored (2026-09-04) +------------------------------------------------ + + The GPUI issue was a missing cached checkout, not a broken pin or API. + The pinned commit exists and is dated 2026-08-19; its GitHub signature + status is unverified. Fetched the exact locked dependency sources with: + + cargo fetch --locked --target aarch64-apple-darwin + + Inspected the GPUI/GPUI-macOS build entry points: local resource/binding + generation and shader preparation, with no unexpected network step. + No dependency versions, repository security settings, or GPUI source + files were changed. Cargo.toml and Cargo.lock remained unchanged. + + Normal workspace commands now work: + cargo test -p zeron-crypto --locked --offline + cargo check --workspace --all-targets --locked --offline + cargo test -p zeron-doc -p zeron-sync -p zeron-rpc -p zeron-engine --locked --offline + + Results: crypto's 17 tests passed; the all-target workspace check passed; + the doc/sync/RPC/engine unit and integration suites passed after the + test synchronization correction below. Existing warnings remain; live + provider/edge tests retain their existing explicit ignored status. + + The broader run reproduced a pre-existing heartbeat test race twice. + Its "run completes" predicate accepted ANY complete message, including + the user entry created before the harness had emitted reasoning. The + test then read the journal too early. Tightened the predicate to require + a complete ASSISTANT entry; no assertion was removed and no production + behavior was changed. The formerly failing test and the broader suite + then passed. + + The old isolated-manifest commands are historical evidence only. Use + the real workspace from here onward; do not preserve a workaround for + a checkout that simply needed its pinned dependencies fetched. + + +15. Content Encryption Implemented (2026-09-04) +----------------------------------------------- + + Selected the per-record-derived-key design for implementation, superseding + the undecided status in the historical Section 12 notes. RFC 7.7 specifies + the exact wire, key-derivation, and AAD inputs. No earlier draft ciphertext + is a compatibility target. Release security review still applies. + + Rust content.rs and Swift VaultContent.swift now provide: + - Immutable scoped content-root keys and device signing identities. + - Fresh OS-generated revision/salt material for every new seal. + - HKDF-SHA256 per-record AES-256-GCM keys, one encryption per derived + key, and signed ciphertext envelopes with explicit purpose/key ID. + - Signature/context verification before key selection and AEAD open; + no plaintext or alternate-format fallback on failure. + - Bounded allocations, explicit entropy errors, and redacted key/ + plaintext wrapper diagnostics. Secret export is explicit for future + protected key envelopes, not a general RPC operation. + + Root-key objects are bound to vault/generation/epoch/object. Using one + under a different scope or a signer under another author ID fails before + encryption. Key lifecycle code must generate new root material on rotation; + reconstructing an old secret under new labels is not revocation. + + Shared fixtures demonstrate Swift opening Rust ciphertext and Rust opening + fresh Swift ciphertext. Tests cover mutation/truncation, wrong roots/IDs/ + purposes/scopes/authors, empty payloads, size and entropy failures, distinct + fresh seals, stable retry bytes, and AEAD rejection even after a modified + ciphertext/context is re-signed by the test author. + + Verification: 25 Rust tests passed from the real workspace, 15 native + XCTest cases passed, and the all-target workspace check passed. The + conformance runner now compiles VaultContent.swift alongside VaultCrypto + and VaultSignedRecord. No external dependency version was changed. + + swiftc apps/ios/Zeron/Sync/VaultCrypto.swift \ + apps/ios/Zeron/Sync/VaultSignedRecord.swift \ + apps/ios/Zeron/Sync/VaultContent.swift \ + scripts/check-crypto-capabilities.swift -o target/crypto-capabilities-probe + ./target/crypto-capabilities-probe \ + crates/crypto/tests/fixtures/primitives.json target/crypto-swift-vectors.json + ZERON_CRYPTO_TEST_VECTORS="$PWD/target/crypto-swift-vectors.json" \ + cargo test -p zeron-crypto --locked --offline + cargo check --workspace --all-targets --locked --offline + + Next is durable outbox/snapshot persistence and key-management integration. + These codecs are working implementation, not a user-facing activation: + bootstrap/pairing/recovery, every content path, migration, and final security + evidence are still required for the complete feature in PR #252. + + +16. Atomic Encrypted Chat Outbox (2026-09-04) +-------------------------------------------- + + Integrated zeron-crypto into zeron-sync and added an encrypted_outbox + table to DocsStore. Only sealed ChatUpdate records can enter this queue; + blob/sidecar/checkpoint work needs its own transport-specific handling. + The queue rejects records larger than ChatClient's existing MAX_PUSH_BYTES + before committing them, and enforces a bounded per-profile byte budget. + + persist_encrypted_batch writes the local snapshot, verified receive cursor, + document lineage epoch, and immutable ciphertext in one IMMEDIATE SQLite + transaction with synchronous=FULL. A PendingEncryptedBatch receipt is + returned only after commit. A duplicate enqueue returns the existing bytes + without overwriting a newer snapshot; reusing an ID for another document + or different bytes is rejected. Cursor overflow/regression is rejected. + Crypto key epochs use 8-byte big-endian blobs, separate from the existing + document epoch, so full-width u64 values do not wrap through SQLite i64. + + Replay requires the caller's trusted current binding and author public + key, filters every binding dimension, verifies stored record signatures, + and preserves FIFO order. Old-epoch/policy records remain stored rather + than being sent as current or silently discarded. Reads are bounded to + 128 batches and 64 MiB; enqueue quotas are checked under the write lock. + Acknowledgment deletes only the exact receipt/bytes and is idempotent; + it never deletes the snapshot or processed-command ledger. Corrupt rows + stop replay and are retained for recovery instead of being acknowledged. + + Nine new integration tests cover restart/ack, duplicate/stale enqueues, + transaction failure after snapshot update, quota/cursor failures, exact + policy filtering, corrupted ciphertext, stale acknowledgments, purpose/ + frame-size rejection, full-width key epochs, and concurrent connections + competing for a one-record quota. All pass. The normal all-target + workspace check and crypto/doc/sync/RPC/engine suites also pass, with + their existing warnings and explicitly ignored live-service tests. + + The caller must serialize per-document mutations and supply a cursor + already verified by the receive path. These APIs do not manufacture + trusted membership or connect encrypted traffic to the legacy transport. + Local snapshots remain plaintext under the existing endpoint-data policy; + no private content keys are stored in the outbox. + + Remaining work in this same PR: secure key persistence and bootstrap/ + membership/key envelopes; pairing/recovery/revocation; desktop/iOS vault + gates and native durable storage; wire the committed batches and verified + apply outcomes into WS/HTTPS; cover registry/RPC/sidecars/blobs; migrate + plaintext profiles; finish end-to-end leakage, failure, and security review. + + + +17. Control Plane, Vault Service, and Sealed Chat Transport (2026-09-07) +----------------------------------------------------------------------- + + This slice implements ES-04, ES-05, ES-06 (comparison-code pairing and + kit recovery), ES-08, most of ES-09, the desktop/CLI parts of ES-07, and + the writer side of ES-12's tail/blob/diff paths. It is working code with + live evidence, not activation: the UI exposes the vault only as an opt-in + Settings section and no migration/fencing of legacy plaintext exists yet. + +17.1. Cryptographic records (crates/crypto) + + - hpke.rs: RFC 9180 base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, + AES-256-GCM, assembled from ring (HMAC/HKDF/AES-GCM) and curve25519-dalek + 4.1.3 (X25519 ladder; ring has no static-key agreement). Pinned to the + RFC A.1 vectors: KEM shared secret, key schedule, sequence-0 ciphertext. + Low-order peer points and oversized inputs are rejected. + - policy.rs: signed membership records (RFC §7.8). Genesis is self-signed + by the single initial device at epoch 1; AddDevice keeps the epoch, + RevokeDevice / RotateRecovery / RecoveryTransition bump it. Records are + parent-linked by membership hash; the recovery authority signs + transitions under an ID derived from its public key. EnrollmentRequest + proofs and the "NNNN-NNNN" pairing code (which now includes the genesis + hash, so a relay that presents a substitute vault is visible) live here. + - keyring.rs / envelope.rs / recovery.rs: epoch keyring codec, HPKE + keyring envelopes for devices and the recovery authority, per-object + content keys wrapped under epoch keys (first writer wins), and the + grouped checksummed Base32 recovery kit with labeled key derivation. + - tests/fixtures/vault.json is generated by tests/vault_fixture.rs and + verified by Rust, by the edge's vault-records.test.ts, and (next) Swift. + + Supply chain: curve25519-dalek 4.1.3 archive checksum + 97fb8b7c…b2be matches crates.io (published 2024-06-18 by rozbb, not + yanked, BSD-3-Clause). Its build script only selects a backend cfg; no + process, network, or filesystem access was found in the scanned sources. + The edge tests needed the project's own locked npm dev dependencies + (npm ci --ignore-scripts); no new npm package was added. + +17.2. Edge control plane (edge/src/vault-room.ts, vault-records.ts) + + One VaultRoom Durable Object per (orgId, userId), name vault1/{org}/{user}, + routed at /vault/:orgId/* with the registry's org check. It stores only + public and encrypted material and verifies every write cryptographically: + membership appends are CAS on the parent hash and signature-checked + (WebCrypto Ed25519 with the RFC §7.6 prechecks) against the stored head; + keyring and object-key envelopes must be signed by an active device under + the current head; enrollment requests carry a device-possession proof. + Object keys are first-writer-wins per (object, epoch): a 409 returns the + stored record and the client adopts it. Enrollment requests expire after + 15 minutes; approval is a bookkeeping mark that requires the membership + record and the device's envelope to already exist. + + The DO cannot unlock, reset, or decrypt anything; a bearer alone can read + descriptors and records but cannot alter trust. Server-side validation is + hygiene, not the trust anchor: clients re-verify from local pins. + +17.3. Engine vault service (crates/engine/src/vault) + + - store.rs: vault.json under the profile store root, AES-256-GCM under a + 32-byte device protection key from the macOS Keychain (security CLI, + account = org/user), a systemd credential (zeron-vault-key), or an + explicit ZERON_VAULT_KEY_FILE (0600). No provider means Locked; the + file is never read or written in the clear. Secrets never print. + - service.rs: explicit phases (RFC §4.3) published on a watch channel; + setup (genesis, keyring epoch 1, device + recovery envelopes, local pin + only after everything is durable), refresh (membership pull with + rejection on any record the pinned history refuses, keyring pull, + owed-envelope retry, enrollment polling), request_enrollment, approve + (code re-derived from server-reported keys and the approver's pinned + genesis; the add record is posted, then the envelope, with the envelope + recorded as owed until it lands), revoke (RevokeDevice + fresh epoch key + + owed envelopes to every retained recipient), recover (chain verified + against the kit's authority, recovery envelope opened, RecoveryTransition + under a fresh epoch), and seal/open material: opening rebuilds the + TRUSTED binding from the membership revision the record names and the + author's key at that revision; the parsed binding is never trusted. + - Vault* RPC methods; Settings -> Encryption (crates/ui/src/settings/ + encryption.rs); zeron vault status|setup|pair|requests|approve|reject| + revoke|recover (apps/zeron/src/vault_cli.rs). + +17.4. Sealed chat transport (crates/sync, crates/engine) + + - ChatDocSink returns ApplyOutcome (Applied / KeyUnavailable / + AuthenticationFailed / Unsupported / StorageFailed). The client commits + the cursor only on Applied; pausing outcomes hold the cursor, drop rows + above it, keep pushes flowing, and are announced as SyncPaused; resume() + backfills from the honest cursor. Tests: unverifiable_row_holds_cursor… + and sealed_batches_keep_their_ids…. + - enqueue_sealed(batch_id, bytes) queues immutable outbox bytes under the + revision-id batch id; acks and permanent rejections reach the sink so + the durable copy is retired. + - EngineChatSink (chat2_host.rs) opens rows/checkpoints/frontiers through + ChatCodec before Loro sees them; a frontier that cannot be opened is NOT + contained. seal_and_queue commits snapshot + verified cursor + ciphertext + in one transaction; replay_outbox re-seals batches whose policy is stale + (RFC §11) and retires the old copy. + - Encrypted profiles use a separate room per chat (chat2/{id}-e1, doc + epoch 3) so ciphertext and legacy rows never share a log and the + plaintext copy is retained for the separate cleanup step. The host + seeds the room through the existing bootstrap checkpoint. Joins wait + for Ready; locked/key-update/revoked states never fall back. + - Checkpoint + frontier, tail, tool blobs (writer and reader), and the + diff sidecar (PUT chat2/{room}/diff) are sealed for enrolled profiles. + +17.4a. Registry field envelopes (ES-10, first cut) + + - zeron-sync RegistryCodec seam: seal_ops / open_rows wrap the client's + push and every inbound row (hello state, HTTPS pull, live broadcasts). + A batch is never half-sealed (keys not ready keeps it pending); a row + with a field whose key is not held is withheld and the doc cursor is + held (RegistryDoc::hold_cursor) so the next pull re-delivers it; a + field that fails verification is dropped, never the row's verified + fields or the clocks. + - Engine VaultRegistryCodec: purpose RegistryField, one object key per + epoch for the whole registry (object "registry"), plaintext + {kind, id, field, hlc, value} so the authenticated slot must equal the + slot the server filed it under; value null is an authenticated deletion + marker; wire value {"e1": base64 record}. Plaintext where ciphertext is + required is rejected. + - Encrypted profiles join /registry/{org}/e1/ws → reg1e1/{org}/{user} + (edge/src/index.ts): a separate generation seeded from the local doc + with ORIGINAL clocks by the existing re-seed path; the plaintext + registry room is never written again and is retained for cleanup. + - Still open in ES-10: signed row lifecycle proofs (delete/revival + evidence survives compaction), GC/full-sync rules with proofs, the + Swift registry codec (iOS shows an empty sidebar for an encrypted + profile until it lands), and the reviewed rekey/cutover design. + +17.5. Verification + + cargo test -p zeron-crypto --locked --offline (42 + fixture) + cargo test -p zeron-sync --features mock-server --offline (33 client) + cargo test -p zeron-engine --offline (130 unit + suites) + npm --prefix edge run typecheck && npm --prefix edge run test + (cd edge && npx wrangler dev --port 27640 --var AUTH_MODE:dev --local) + ZERON_VAULT_EDGE_URL=http://127.0.0.1:27640 \ + cargo test -p zeron-engine --test vault_e2e (2 live scenarios) + cargo test -p zeron-sync --features mock-server --test registry_client + xcrun swiftc -typecheck -target arm64-apple-ios26.0-simulator \ + -sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" \ + apps/ios/Zeron/Sync/Vault{Crypto,SignedRecord,Content,Policy,Envelope}.swift + swiftc -parse-as-library apps/ios/Zeron/Sync/Vault*.swift \ + scripts/check-vault-fixture.swift -o target/vault-fixture-probe + ./target/vault-fixture-probe crates/crypto/tests/fixtures/vault.json (29 checks) + + Swift (apps/ios/Zeron/Sync/VaultPolicy.swift, VaultEnvelope.swift) now + verifies the membership chain, opens keyring envelopes with CryptoKit's + HPKE (interoperating with the Rust RFC 9180 implementation), unwraps + object keys, parses/derives the recovery kit, and checks enrollment + proofs and pairing codes; ZeronTests/VaultControlPlaneTests.swift holds + the same assertions for the app test target. + + The live e2e passed: A sets up; B pairs (wrong code refused, matching code + approved); A seals and B opens with a control-plane object key; both + converge on one key; A revokes B (epoch 2), B reports Revoked, cannot get + epoch-2 material, still opens accepted epoch-1 history; C recovers with + the kit (epoch 3) and opens both; A catches up to epoch 3. + +17.6. Remaining in this PR + + - Swift: native Keychain vault store, bootstrap client, pairing/recovery + screens, encrypted SessionStore/ChatRoomClient path, tail/blob readers. + (The record verifiers and fixture test landed in this slice.) + - Registry lifecycle proofs and the Swift registry codec (ES-10 rest); + the device RPC channel handshake (ES-11); device sidecars; the reader + side of sealed tails on iOS. + - Legacy write fences on chat2/blob/diff routes for activated profiles, + migration journal and consented cleanup (ES-14); retention verification. + - Accepted-history closure after revocation (old-epoch writes are still + relayed by the opaque log; readers accept them as history — G3). + - Independent security review; performance baselines; iOS device tests. + + +17.7. Fail-closed local vault and relay boundaries + + Existing vault files (including unreadable/corrupt files) keep the local + encryption requirement even when no keyring can be loaded. Discovering a + remote vault also requires encryption before this device is approved. + Storage failures lock key use; verification failures block cached sealing + and opening material. Tests reproduce the former plaintext classification. + + The device relay now wraps the local RPC service: all Vault* operations + are local-only, and encrypted profiles refuse application RPC over the + unencrypted relay. Outstanding streams close when encryption is required; + engine forwarding/dial entry points also refuse that transport. This is + an intentional capability restriction until the authenticated encrypted + channel exists, not an implementation of that channel. + + Verification: cargo test -p zeron-engine --lib --locked --offline + (134 tests). Bootstrap journaling, live profile cutover, complete route + fencing, native iOS lifecycle, and the remaining security gates are open. + + +17.8. Preserve verified registry fields + + Inbound field verification now merges against the authoritative local + baseline: failed, omitted, and older fields cannot erase verified values + or advance their clocks. Authenticated null remains a field deletion. + Unauthenticated clock-only entries are ignored. A full sync with withheld + rows applies only a delta and holds the cursor, rather than replacing + cached rows and spuriously enqueueing them for re-seeding. + + Verification: zeron-doc and zeron-sync suites with mock-server enabled + pass (87 doc unit tests, 35 sync unit tests, 9 encrypted-outbox tests, + 13 registry integration tests, plus the document integration test). + Row lifecycle proofs and immutable registry retry records remain open. + + +17.9. Resumable vault changes and recovery confirmation + + Setup, approval, revocation, and recovery journal the signed membership + intent and associated key material in the protected local store BEFORE + publishing the membership record. An ambiguous response is reconciled + against the exact record at its expected sequence. Conflicting remote + history retains the intent and pauses rather than resetting local trust. + Refresh resumes pending publication, envelopes, and approval bookkeeping. + Owed envelopes are persisted before sending and retried byte-for-byte; + a changed membership head requires a newly sealed envelope revision. + + Bootstrap keeps the recovery secret only in protected pending setup state + until explicit local confirmation. The desktop "I saved it" action and + `zeron vault confirm-recovery` durably acknowledge it; until then the phase + is RecoveryConfirmationRequired and content key use is blocked. Setup can + redisplay an unconfirmed kit after restart. This does not activate a + complete encrypted-sync release: the UI labels remaining coverage as a + preview. Self-revocation must be performed from another approved device. + + The state file uses exclusive random temporary names, a size budget, and + directory fsync on Unix after rename. Malformed non-ASCII hex is rejected + without panicking. Three loopback HTTP fault tests cover rejected and + ambiguous membership writes, envelope failure, process reopen, identical + retries, confirmation, and retention of the exact staged rotation key. + Workspace all-target check, crypto tests, and engine suites passed; + live-service tests were not executed in this pass. + + Broader verification also reproduced a peer-link sign-out bug: lagged + connectivity broadcasts terminated the credential watcher. The watcher + now survives lag and subscribes to credentials before spawning; the RPC + device-room suite passes with deterministic event overflow coverage. + + +17.10. Relay refusal gates and runtime coverage + + The pending edge framing/fence work now checks the encrypted payload's + format, suite, and purpose as well as the signed wrapper. Registry values + must be purpose-4 field envelopes; chat updates, checkpoints, frontiers, + tails, diffs, and blobs must match their respective purposes. This is + framing hygiene only, not server-side signature or decryption authority. + + Only an explicit absent-vault response permits plaintext. Lookup errors + fail closed, absence is never memoized, and the positive refusal cache is + bounded. The Worker overwrites the internal org stamp from authenticated + claims. Chat/registry sockets retain that scope and check again on pushes; + legacy session sockets also check before processing binary frames. Device + relay bytes and plaintext device-sidecar writes are refused for encrypted + profiles until an authenticated encrypted device channel is implemented. + + VaultRoom rechecks its membership head after asynchronous signature work + and before SQL writes, so concurrent membership/envelope operations cannot + commit using a superseded authorization decision. + + Verification: edge typecheck, 51 Node tests, and 18 actual-workerd tests. + Seven new runtime cases exercise existing-socket fencing, encrypted-room + HTTP refusal, device relay/sidecar refusal, competing genesis requests, + and identical sealed bytes across WS push, HTTPS retry, and HTTPS pull. + The offline Rust crypto/doc/sync/RPC/engine suites and workspace all-target + check pass. Clippy completes with existing warnings; an obsolete test + helper loop and the new fault test's guard scope were cleaned up. + + These are request/frame refusal gates, NOT atomic migration activation. + Client-side quiescence and live mode cutover must prevent plaintext from + being serialized at all; refusal cannot undo bytes already transmitted. + Authoritative legacy ownership mappings and a cross-room cutover barrier + remain release blockers, as do native iOS lifecycle/live sync, encrypted + device channels, registry lifecycle proofs/immutable retry records, + accepted-history closure after revocation, immutable checkpoint provenance, + and independent security review. No production deployment was performed. + + + +17.11. iOS vault lifecycle and encrypted transports (2026-09-08) + + The native app now decides its transport mode from the vault BEFORE any + store dials (AppModel.refreshVault → AppConfig.syncAccess): an explicit + absent-vault answer permits the legacy plaintext rooms, an approved + membership with the current epoch key permits the encrypted generations + (chat2/{id}-e1, registry e1), and every other state — pending, locked, + key update required, verification failed, revoked, unknown — blocks all + sync. Sign-out invalidates the config so no late refresh or push can use + the old identity. Foregrounding re-checks a pending approval. + + MobileVault (apps/ios/Zeron/Sync/VaultSession.swift) keeps the pinned + genesis fingerprint, verified history, keyring, object keys, enrollment, + and journaled recovery transitions in a Keychain-protected state file + (VaultPersistence: device-only, non-syncing key; AES-GCM; excluded from + backup; fsync'd atomic writes). Enrollment requires the vault fingerprint + from an approved device; the phone shows the genesis-bound comparison + code. Recovery uses the kit plus the fingerprint from the recovery file. + + SessionStore seals local updates into a durable per-chat outbox + (VaultChatDisk) before any transport sees them, re-seals stale batches + under the current head, retires them on ack, and opens rows/checkpoints/ + frontiers through the vault with explicit outcomes (ChatApplyOutcome): + unverifiable data pauses the room and holds the cursor. WorkspaceStore + seals every registry value through VaultRegistryCodec, keeps sealed + batches durable until acked, and merges opened rows against the verified + baseline so rejected fields cannot erase verified ones. + + The Encryption screen (Views/EncryptionView.swift, Home → account menu) + shows the state, fingerprint, epoch, and comparison code, and offers + "Approve from another device" and "Use recovery key". + + Verification: xcodebuild build for the iOS simulator succeeds with the + whole app; the ZeronTests target runs on an iPhone 17 Pro simulator + (see the PR test plan for the count). Not yet covered: live two-device + pairing on hardware, background/lock lifecycle, and the iOS readers for + sealed tails/blobs (the app reads transcripts from the sealed rows). + +17.12. Authenticated device channel (ES-11, 2026-09-09) + + Device RPC for enrolled profiles now rides a Noise XX session between + the two devices' vault identities (RFC §10, profile + Noise_XX_25519_AESGCM_SHA256 via the `snow` crate, statics = the + membership-published X25519 encryption keys, prologue bound to vault id + and generation, device ids inside the encrypted handshake payloads). + crates/crypto/src/channel.rs wraps the handshake and the transport + (chunking, frame cap, per-direction budget); crates/rpc/src/ + device_channel.rs defines the frame shape (kind "chan", stream ids + hs1/hs2/hs3/rpc/err) and the ChannelAuthority seam; crates/rpc/src/ + device_room.rs runs the responder in the host relay and the initiator + in DeviceLink/LinkCache. The engine implements ChannelAuthority on + VaultService (identity from the local store, scope and membership from + the pinned head, self-connections refused) and serves a second + RelayRpc instance to channel-authenticated conns; the plaintext + instance keeps refusing everything once enrolled, and both refuse + Vault* methods. Membership is re-checked on every inbound sealed frame; + a revocation that has reached either side ends the session. + + The edge DeviceRoom relays "chan" frames (and payload-less liveness + echoes) for encrypted profiles and closes the socket (4403) on any + plaintext RPC frame, so the relay never carries application plaintext + for an enrolled profile. The former blanket refusals in dial_device and + EngineRpc::forward are gone: the link cache enforces the channel. + + Verification: 3 crypto tests (mutual authentication, chunking/replay/ + tamper, membership + prologue gating); 5 relay tests over the in-memory + fake DO (RPC + streams + 200 KB frames with zero plaintext frames on the + relay, plaintext client refused by an enrolled host, non-members refused + on either side, mid-session revocation, unenrolled profiles keep + plaintext); the workerd gate test now proves "chan" frames cross the + relay while plaintext closes; a live scenario in + crates/engine/tests/vault_e2e.rs (device_channel_over_the_live_relay) + pairs two VaultServices through a real DeviceRoom and checks the same + properties. Not yet covered: the iOS relay client (the phone keeps + refusing relay RPC until a Rust bridge exists), and per-session audit of + which member issued which command. + +17.13. Authenticated registry row tombstones (ES-10, G4 first half; 2026-09-09) + + Deletes in the encrypted registry generation now carry a row lifecycle + proof: a content record with purpose 9 (RegistryLifecycle) whose + authenticated plaintext is {kind, id, op: "delete", hlc}. The room + refuses a bare delete for an encrypted generation (plaintext_rejected), + stores the proof as the tombstone's delProof (new column, added in + place for existing rooms), serves it with the row, and drops it on + revival. Readers in all three languages accept a tombstone only with a + verified proof for exactly that row and clock that is causally newer + than the verified baseline, and accept a live row over a verified + tombstone only with a verified field newer than it; a proof whose key + is not held withholds the row (cursor holds). The relay can therefore + neither delete nor resurrect a row. iOS previously paused on any + tombstone in an encrypted room; it now verifies them. + + Files: crates/crypto/src/content.rs (purpose 9), crates/doc/src/ + registry.rs (proof/del_proof on ops and rows, seed round-trip), + crates/sync/src/registry.rs (codec seal/open_lifecycle, tombstone and + revival rules in open_rows), crates/engine/src/vault/registry_codec.rs, + edge/src/{registry-core,registry-room,vault-records}.ts, apps/ios/ + Zeron/Sync/{RegistryCore,VaultRegistryCodec,VaultContent}.swift; the + shared fixture gained registryLifecycleRecord/Plaintext. + + Verification: doc 89; sync 39 + 9 + 14 (incl. a forged tombstone from a + keyless writer that the server merges and the encrypted peer ignores, + while the member's proven delete lands); crypto fixture round-trip + (opens as purpose 9, refused as purpose 4); edge 53 Node + 19 workerd + (bare delete and wrong-purpose proof refused, proven delete stored and + served byte for byte); Swift RegistryCoreTests lifecycle cases. Still + open in ES-10: GC/full-sync rules with proofs beyond the gcFloor + resync, the reviewed rekey/cutover design, and per-row creation + evidence (field records already bind their clocks). + + On immutable retry records: the Rust registry outbox re-seals a pending + batch on every push attempt. That is safe by construction here — each + seal draws a fresh 32-byte salt (a fresh per-record key, so the zero + nonce is never reused) and Rust's Ed25519 signing is deterministic — + so the RFC §7.3 concern applies to Swift, whose CryptoKit signatures + are not byte-deterministic; iOS already persists sealed batches and + resends the stored bytes. + + + -- End of Project Plan -- diff --git a/docs/ios-encrypted-device-test.md b/docs/ios-encrypted-device-test.md new file mode 100644 index 000000000..efa5b6f37 --- /dev/null +++ b/docs/ios-encrypted-device-test.md @@ -0,0 +1,147 @@ +# Test encrypted sync on a Mac and iPhone + +This branch supports the phone's encrypted device RPC, recent-message tails, +and full tool output/diff sidecars. Pairing refreshes automatically while the +Encryption sheet is open. Desktop control uses the same fixed Noise XX profile +as the Rust client, with membership checks and no plaintext fallback. + +After the recovery kit is confirmed, the desktop automatically copies existing +chat history into encrypted storage. Settings → Encryption shows progress and +offers retry if a source or referenced tool output is unavailable. Copies are +read back and verified before each chat is marked complete; interrupted work +resumes on restart. Original plaintext copies are retained, so this does not yet +erase plaintext history from the relay. History held only on an offline desktop +is copied when that desktop joins encryption and comes online. Keep desktops +online until their migrations finish before relying on phone-only access. +The production worker must be updated separately before these paths can work +against production. + +## Start the branch locally + +Keep the Mac and phone on the same trusted Wi-Fi network. Local dev accepts +synthetic identities; do not expose this dev server to the public internet. + +From the repository root, start the edge in one terminal: + +```sh +cd edge +npm ci +npx wrangler dev --local --ip 0.0.0.0 --port 27640 \ + --var AUTH_MODE:dev --persist-to .wrangler/mobile-device-test +``` + +Use the repository-pinned Wrangler version: newer 4.119 builds crashed during +local WebSocket testing. Keep this terminal running while testing the devices. + +In another terminal, start a separate desktop profile and IPC port: + +```sh +export ZERON_DATA_DIR="$HOME/Library/Application Support/ZeronMobileTest" +export ZERON_IPC_PORT=27655 +export ZERON_EDGE_URL="http://$(scutil --get LocalHostName).local:27640" +export ZERON_EDGE_TOKEN=mobile-test@mobile-test +export ZERON_ORG_ID=mobile-test +cargo run -p zeron +``` + +Allow macOS incoming connections if prompted. The phone's edge URL must use +the Mac's `.local` hostname, never `localhost` (which would mean the phone). +Use exactly the same edge URL on both devices throughout this test; the +phone's local vault storage is scoped to the origin as well as the account. + +On the desktop, open **Settings → Encryption**, create a vault, and save and +confirm the recovery kit. Copy its full vault fingerprint. + +## Install and pair the phone + +1. Connect and unlock the iPhone. Enable Developer Mode if Xcode requests it. +2. Open `apps/ios/Zeron.xcodeproj`, choose the phone, and run the **Zeron** + scheme with your existing signing team. Under **Edit Scheme → Run → Info**, + choose the **Debug** build configuration. +3. Tap **Dev sign in** below **Log in to Zeron** (Debug builds only). Use the edge URL printed by + `echo "http://$(scutil --get LocalHostName).local:27640"` on the Mac, + user `mobile-test`, and organization `mobile-test`. Allow Local Network + access when iOS asks. +4. Open **Encryption**, paste the desktop's fingerprint, and tap + **Approve from another device**. +5. The running desktop checks for requests every five seconds and posts a + notification for each new request, including reapproval. Click **Review** + in the sidebar to open Encryption settings. System banners follow the + desktop notification toggle and macOS notification permissions. + On the desktop, approve the pending phone only after comparing the full + eight-digit code on both screens. Keep the phone's Encryption sheet open; + it should switch to **Encrypted** automatically. + +The CLI equivalent for the isolated desktop's approval is: + +```sh +ZERON_IPC_PORT=27655 target/debug/zeron vault requests +ZERON_IPC_PORT=27655 target/debug/zeron vault approve REQUEST_ID CODE_FROM_PHONE +``` + +## Exercise the real devices + +In desktop **Settings → Encryption → Approved devices**, use **Rename** to +choose names such as Laptop or iPhone. Labels sync encrypted and remain +available for removed identities. + +- Create a new space from the phone. Browse a desktop folder and create a + chat; this exercises encrypted RPC, not just mirrored chat data. +- Send a prompt from each device and watch both transcripts converge. +- Attach an image from the phone and check that the desktop receives it. +- Run a tool with long output. Expand its activity row and choose **Show full + output**; **Show full diff** appears when the host published a diff reference. +- Open a cold chat: its sealed recent-message tail can render while the full + checkpoint and log load. A tail never advances the sync cursor. +- Background the phone, bring it back, and repeat folder browsing and sending. +- With the recovery kit saved, revoke the phone from the desktop. Remote + control must stop. While active, the phone checks membership every five + seconds and shows **Get approval to view your chats**. Tap **Ask for + approval**, compare the large code with the desktop, and approve it. + Recovery is available separately; connection details are behind the info + button. Foregrounding also checks immediately. Verify the phone returns + to the home screen automatically after approval. + +A missing sidecar shows an error with Retry; the full transcript continues +loading through the checkpoint/log path. Sidecar reads do not require the +host desktop to remain online after it has published them. + +## Repeatable automated check + +With Xcode and the edge's npm dependencies installed: + +```sh +scripts/test-ios-vault-live.sh +``` + +The script starts an isolated local worker on port 27641 and a Rust HostRelay, +creates a disposable vault, then runs the actual iOS clients on the simulator. +It tests enrollment, concurrent cold RPC calls, streaming replies, large +chunked requests/replies, sealed tails and blobs, reconnect, and revocation. +It also runs deterministic Snow/Swift conformance and sidecar rejection tests. +Automatic approval exists only in this disposable test host. + +The Rust migration test uses the real local worker and covers a checkpoint +larger than the row size limit, plaintext checkpoint plus final rows, missing +tool output, restart and retry, recovery-kit reads, retained originals, and +preservation of message IDs and the processed-command ledger: + +```sh +ZERON_VAULT_EDGE_URL=http://127.0.0.1:27640 cargo test -p zeron-engine --test vault_e2e plaintext_history_migrates +``` + +Logs are in `/tmp/comet-mobile-e2e`. Override the simulator with +`ZERON_IOS_TEST_DESTINATION='platform=iOS Simulator,name=iPhone 17 Pro Max'`. +The live test skips during normal test runs unless its companion host has +created a connection file. + +To check or regenerate the deterministic Rust fixture: + +```sh +cargo test -p zeron-crypto --test channel_fixture +UPDATE_CHANNEL_FIXTURE=1 cargo test -p zeron-crypto --test channel_fixture +``` + +Protocol references: [Noise specification](https://noiseprotocol.org/noise.html), +[Rust channel implementation](../crates/crypto/src/channel.rs), and +[Wrangler local development](https://developers.cloudflare.com/workers/wrangler/commands/dev/). diff --git a/docs/rfc-0001-encrypted-sync.txt b/docs/rfc-0001-encrypted-sync.txt new file mode 100644 index 000000000..026f46ea8 --- /dev/null +++ b/docs/rfc-0001-encrypted-sync.txt @@ -0,0 +1,1449 @@ +Zeron Project September 2026 +Request for Comments: 0001 Draft 00 / Phase 1 +Category: Product and Security Design Date: 2026-09-04 +Code baseline: a0650ab + + + END-TO-END ENCRYPTED WORKSPACE SYNCHRONIZATION + ============================================ + +Status of This Memo + + This is a local project RFC, not an Internet standard. It proposes the + product contract and security architecture for encrypted Zeron sync. + It does not describe a shipped feature or authorize a deployment. + + Phase 1 produces this document for review. No encryption implementation, + dependency installation, migration, or deletion is part of Phase 1. + Acceptance of this RFC does not replace review of the exact protocol. + The blocking decisions in Section 15 must be resolved before the + corresponding implementation is considered production-ready. + + MUST, MUST NOT, SHOULD, and MAY express requirements of the proposed + design. Diagrams and record layouts describe logical structure, not + wire encodings. References [R1]-[R12] identify current implementation + seams; they are not claims that the proposed behaviour already exists. + +Abstract + + Synced transcripts can contain source code, file contents, commands, + patches, and command output. TLS and authenticated backend access do + not prevent a backend operator or a storage compromise from reading + those contents. Zeron's opaque chat log makes client-side encryption + practical without moving CRDT interpretation back into the server. + + This RFC proposes one encrypted vault per personal synced workspace. + Users sign in normally, approve devices once, and save a recovery key. + All content-bearing sync paths are encrypted, including the registry, + sidecars, and device RPC. The server retains routing, ordering, and + field-level merge duties, but receives no content decryption keys. + + The release unit is complete encrypted sync, not encrypted chat blobs + with plaintext copies elsewhere. Local-only operation remains separate. + +Table of Contents + + 1. Decisions and scope + 2. Threat model and product promise + 3. Current data paths and exposure inventory + 4. User experience and state machines + 5. Key hierarchy and device authority + 6. Enrolment, recovery, and bootstrap service + 7. Cryptographic records and protocol requirements + 8. Chat log, checkpoint, and cursor integration + 9. Registry encryption without changing merge semantics + 10. Device RPC, attachments, and other content paths + 11. Revocation and key rotation + 12. Migration, compatibility, and old plaintext copies + 13. Local storage, observability, and operational limits + 14. Implementation sequence and acceptance tests + 15. Blocking review decisions and alternatives + 16. Repository references + + +1. Decisions and Scope +---------------------- + + D1. Encryption is opt-in per personal synced workspace for the first + release. Within that workspace it is all-or-nothing, not a toggle + for individual chats, payload types, or spaces. + + D2. The current workspace boundary is the account-scoped profile + (orgId, userId), not the whole organization. Organization membership + or an organization administrator role does not grant vault access. + A random vault ID is bound to that profile in authenticated policy. + + D3. Every approved device is a full-trust workspace peer in v1. It can + read workspace history, write as an authorized peer, and approve or + revoke devices. This includes an approved headless VPS. Per-space + grants, read-only devices, and restricted runners are deferred. + + D4. Account authentication and vault authorization are independent. + Login, password reset, token refresh, and support intervention MUST + NOT supply or replace the authority needed to decrypt a vault. + + D5. Device approval is the normal onboarding path. A high-entropy + recovery key is the disaster-recovery path. No daily encryption + password, server escrow, or automatic email-based key recovery. + + D6. Native Rust and Swift clients ship compatible support together. + Legacy writers must be upgraded or stopped before activation. + Unsupported clients are denied service, but denial cannot make + an obsolete client's outbound request confidential (Section 12). + Encryption MUST NOT silently downgrade on errors or missing keys. + + D7. There is no in-place "turn encryption off" operation in v1. Stopping + sync does not decrypt cloud copies. An explicit future export into + another profile would be a separate, consented operation. + + D8. No encryption format has shipped. The first encrypted-sync release + is one feature developed in one PR; draft codecs may be replaced + directly without compatibility layers for earlier drafts. Existing + plaintext data and clients still require the migration/fencing + treatment in Section 12. Future unknown encrypted formats fail closed. + + Local-only profiles MUST remain local. Enabling encryption or signing + in MUST NOT import local-only sessions, attachments, or agent secrets. + Any existing explicit profile-copy flow must respect the vault gate. + Auth state changes MUST NOT silently change an open storage boundary. + + +2. Threat Model and Product Promise +---------------------------------- + + Proposed product copy: + + "Your synced content is encrypted on your devices. Only devices + you approve, or someone with your recovery key and encrypted + backups, can read it. Our sync backend stores ciphertext." + + The primary goal is confidentiality against backend/database access, + including copies in Durable Object storage and R2 backups. TLS remains + required. Encryption is additional protection, not a replacement for + authentication, authorization, quotas, or safe RPC handling. + + Clients MUST also authenticate content and peer identity. A malicious + relay must not substitute an enrollment key, forge a command, or move + ciphertext from one chat/field into another and have it accepted. + + The service still observes: + + - Account identity and authenticated organization membership. + - IP addresses, connection timing, sizes, and access patterns. + - Random vault/room/object/device IDs and routing relationships. + - Log sequence numbers, merge clocks, field names, and tombstones. + - Device public keys, membership changes, and protocol versions. + + Titles, paths, branches, previews, filenames, device display names, + repository URLs, and content checksums MUST NOT be smuggled into those + visible identifiers, headers, diagnostics, or object metadata. + + Non-goals and limits: + + - A compromised approved device, local malware, or VPS root access. + - Hiding input from the model provider or the agent that executes it. + - Protection from a malicious replacement application binary. + - Automatic encryption of all local files, journals, or OS backups. + - Hiding traffic volume, timing, or every piece of structural data. + - Erasing information a device or operator previously copied. + - Availability against a server that deletes or withholds data. + - Perfect detection of server rollback, omission, or split views. + + Known state must be pinned locally and regressions rejected. Signed + records do not, by themselves, prove that the server supplied the most + recent record or all records. A fresh recovery client may have no recent + state with which to detect a stale but valid backup. Stronger global + freshness guarantees would require an additional consistency design. + + This feature is not a claim that work "never leaves the machine" or + that a particular client/NDA policy permits a VPS or model provider. + + +3. Current Data Paths and Exposure Inventory +------------------------------------------- + + The baseline has several paths outside the opaque Loro update stream. + Each path below MUST be covered or disabled for an encrypted profile. + + Surface Current behavior / required treatment + ------------------------- ------------------------------------------- + Chat rows Opaque update bytes in ChatRoom. Encrypt + before enqueue, verify before Loro import. + + Checkpoints and frontiers Client publishes both. Encrypt snapshot + and frontier; bind them to one revision. + + Chat tail doc_host publishes transcript JSON to + /chat2/{id}/tail. Encrypt independently. + + Working-tree diff diff_sync still posts path, branch, and + patch to legacy /diff/{id}. Migrate this + path; a chat-row wrapper cannot cover it. + + Workspace registry Server merges individual field values. + Encrypt values, retain minimal merge data. + + R2 tool blobs /blob stores output/diff payloads. Finished + subagent transcripts also use this path. + Encrypt all such payloads and manifests. + + User attachments Host-local staging and device RPC, not + current R2 attachment mirroring. Protect + upload and read RPC payloads end to end. + + Device relay RPC includes files, folders, terminal I/O, + and control requests. Authenticate peers + and encrypt before entering DeviceRoom. + + Device sidecars JSON storage slots exist. Encrypt all + content-bearing values if used. + + Presence and nudges Keep only minimal routing/liveness data + clear; encrypt any content-bearing detail. + + Backups and diagnostics Back up ciphertext plus verifiable public + metadata. Never log decrypted payloads. + + The old /attachments PUT route acknowledges and discards data. Its + existence does not prove historical R2 attachment objects were deleted. + Likewise, a dormant upload path is not permission to leave it unsafe: + re-enabling a call site must not bypass the encryption boundary. + + Sources: [R1]-[R8]. Older architecture notes contain historical plans; + implementation behavior at the stated baseline controls this inventory. + + +4. User Experience and State Machines +------------------------------------ + +4.1. First activation + + Settings -> Sync -> End-to-end encryption: + + +------------------------------------------------------------+ + | Protect this workspace | + | | + | Sessions, files and workspace details are encrypted before | + | they reach our servers. Approved devices can read them. | + | | + | [Not now] [Set up encryption] | + +------------------------------------------------------------+ + + Setup generates keys locally, creates this device's membership, and + presents a recovery kit. The user saves it to a password manager or + local recovery file and confirms possession before activation. + + Required recovery copy: + + "If you lose every approved device and your recovery key, we + cannot recover your encrypted data. Resetting your account + password will not restore access." + + The key is random, grouped for copying, and protected by a typo-detection + checksum. It is not a password the user invents or memorizes. The kit + identifies the vault and recovery authority without requiring email + addresses, folder names, or other identifying labels. + + Existing accounts enter migration (Section 12). The UI MUST NOT show + "Encrypted" while a content path still writes plaintext. It separately + reports retained legacy copies even after new sync is encrypted. + +4.2. New phone or computer + + Normal account login is followed by: + + +------------------------------------------------------------+ + | Approve this device to unlock your workspace | + | | + | [Approve from another device] | + | [Use recovery key] | + +------------------------------------------------------------+ + + QR pairing is the default. A comparison-code flow is the alternative + for devices without a usable camera, including headless machines. + An existing device explicitly approves the authenticated pairing. + An untrusted display name alone is never proof of device identity. + + Pending devices MUST NOT publish their names, paths, registry writes, + content, or general RPC before approval. They may use the minimal + authenticated bootstrap service. Rejecting or expiring an enrollment + must leave the existing workspace unchanged. + +4.3. Independent state dimensions + + Account: signed out | signed in + Profile: local | legacy synced | migrating | encrypted + Vault: not enrolled | locked | ready | key update required + | verification failed | revoked + Transport: offline | connecting | caught up | retrying + + These are not one boolean "connected" flag. In particular, a valid + login and a healthy socket do not imply a usable vault key. + + State User-facing behavior + ------------------------ -------------------------------------------- + Not enrolled "Approve this device"; bootstrap only. + Secure store locked "Unlock this device"; no content upload. + Ready, online Existing streaming and sync experience. + Ready, offline Read permitted local cache; queue edits. + Key update required "Waiting for encryption keys"; preserve data. + Verification failed "Sync paused: data could not be verified." + Revoked Stop encrypted sync and remote control. + + Errors MUST NOT erase a last-known-good transcript, clear a pending + command, show an empty chat as success, or trigger plaintext fallback. + Network retries and crypto failures require different recovery actions. + +4.4. Daily use, sign-out, and locking + + No additional password is required for ordinary reads or sends on an + approved device whose OS secure store is available. Offline operation + continues within the existing local-data policy. + + "Lock sync keys" is not a promise to encrypt or hide all existing local + caches. If an app-lock feature hides local content, it needs its own + cache and memory-lifetime policy. The UI MUST distinguish those claims. + + Account sign-out stops network access and releases in-memory keys as + practicable. Retaining this device's secure-store enrollment for later + login is distinct from revoking it. "Remove this device" revokes its + membership; optional local-data deletion requires explicit confirmation. + +4.5. Headless operation + + A proposed CLI pairing action displays an enrollment request and key + fingerprint/code, not workspace keys or a login bearer. Approval occurs + on a trusted native client. Command spelling is an implementation detail. + + The approval prompt MUST disclose that v1 grants the VPS full workspace + trust, including device-management authority, not just job execution. + + Two explicit credential modes are supported: + + Unattended OS/service credential storage allows restart without + a person. The VPS operator/root remains trusted. + + Unlock on boot Sync remains locked until an operator unlocks it + through a local or authenticated pairing mechanism. + + Do not silently fall back from a secure store to a plaintext key file. + If a platform needs an operator-provisioned credential file, it must be + an explicit unattended-mode choice with restrictive permissions and + clear disclosure of the storage and backup implications. + + +5. Key Hierarchy and Device Authority +------------------------------------ + + Keys are independent of WorkOS credentials and existing installation + device IDs. Each vault enrollment has a fresh cryptographic identity. + + Approved device keys Recovery key + | | + +----------+----------+ + | + Recipient envelopes + | + Workspace keyring + | + +-----------+-----------+ + | | | + Registry Chats Blobs + + Logical key material: + + Device identity Separate signing and key-agreement keys, + generated and retained by that device. + + Vault epoch key A random wrapping key for one write epoch. + A new epoch is not derivable from an old key. + + Content keys Separate random keys for chats, registry data, + and blobs, wrapped under a vault epoch key. + Purpose/writer subkeys are domain-separated. + + Recovery secret A random 256-bit secret in the recovery kit. + Separate recovery encryption and signing keys + are derived using reviewed, labeled derivation. + + Devices receive only their own private identity and the workspace + keyring, never another device's private identity. Device private keys + and the recovery secret MUST NOT appear in normal registry fields, + general-purpose RPC responses, logs, or content backups. Persist the + local keyring in secure storage or encrypted under a device-held key, + not as plaintext alongside the application's ordinary snapshots. + + The keyring supplies epoch wrapping keys and the registry entry point. + Chat/blob manifests carry their object-key envelopes, which can be + opened with an already available epoch key. A key needed to open an + object MUST NOT exist only inside that object's encrypted payload. + Key envelopes bind the recipient, vault, object, purpose, and epoch. + + The recovery public encryption key allows approved devices to publish + updated recovery envelopes without keeping or asking for the recovery + secret. This is important: recovery must still work after key rotation, + and daily clients must not need the user's offline recovery key. + + Keyring updates contain the history keys needed for retained data as + well as the active epoch. Keys must not be discarded while referenced + by retained ciphertext, a restorable backup, or pending local work. + Versioned recovery envelopes and device envelopes are committed before + clients publish data that depends on a newly introduced key epoch. + Object-key envelopes likewise become durable before publishing refs + that require them; retries must not strand ciphertext without its key. + + A signed genesis record binds the vault ID, profile, protocol policy, + initial device, and recovery public keys. The first device pins it. + Later devices obtain their trust anchor through authenticated pairing + or their recovery kit, never from a server assertion alone. + + A signed membership history records additions, revocations, recovery + authority changes, and write-epoch transitions. Each change names its + parent revision/hash. An active device may authorize the next change; + the recovery authority may authorize an explicit recovery transition. + Concurrent changes use compare-and-swap and must be re-authorized if + rebasing changes their meaning. Never merge trust decisions by HLC. + + Historical signatures remain verifiable after a device is revoked. + Revocation affects acceptance of new writes, not the existence of valid + historical signatures. Membership history is not a CRDT content log and + must have its own bounded retention/checkpoint design before release. + + +6. Enrollment, Recovery, and Bootstrap Service +--------------------------------------------- + +6.1. Logical bootstrap operations + + The server needs a small key-management control plane independent of + the encrypted registry. These are logical operations, not final URLs: + + Read vault descriptor and signed membership revisions. + Create/read/cancel a short-lived enrollment request. + Publish an approved recipient envelope and membership change. + Read an envelope addressed to this device or recovery authority. + Publish a signed epoch/policy transition with compare-and-swap. + + Account authentication and profile authorization are required in + addition to cryptographic proofs. Rate and size limits apply. Native + clients MUST verify signatures, bindings, and peer possession even if + the service reports success. Server authorization must not rely on a + caller-supplied device ID without proof of its private key. + + The server receives public keys and encrypted envelopes only. It has + no vault-unlock endpoint, administrative decryption key, or plaintext + recovery-secret field. Key absence is not permission to create a new + vault over an existing encrypted profile. + +6.2. Pairing transcript + + New device Untrusted relay Trusted device + | | | + |-- enrollment request -->| | + | |<-- fetch request ---------| + |<==== authenticated pairing; QR/code comparison =====>| + | | user approves + | |<-- signed membership -----| + | |<-- encrypted envelope ----| + |<-- membership/envelope -| | + |-- verify, decrypt, pin | | + |-- possession receipt -->|-------------------------->| + + Pairing MUST bind both device keys, vault/profile identity, request ID, + protocol version, and a fresh challenge. QR data is generated by an + endpoint, not accepted as trusted because a backend web page rendered + it. Enrollment expires, is single-use, and requires explicit consent. + + Use a reviewed authenticated key-exchange/pairing construction. A short + code may be a human comparison of the handshake transcript; it MUST + NOT be used directly as an encryption key. If a typed secret replaces + comparison, use a reviewed PAKE with attempt limits, not a homemade + password exchange. The concrete pairing construction is a review gate. + + Do not auto-enroll every device with a valid WorkOS session. Enrollment + secrets MUST NOT enter query strings, browser history, shell history, + analytics, or remotely captured terminal logs. Bootstrap requests must + not contain a user's email or machine paths as convenience labels. + +6.3. Recovery + + After normal sign-in, the client reads the encrypted recovery bundle. + The user supplies the recovery kit locally. The client derives recovery + keys, verifies the vault binding and authority, decrypts the bundle, + and creates a new device identity. + + Recovery is an explicit signed transition, not a backend membership + override. The user chooses whether unavailable devices are retained or + revoked. Lost/stolen-device recovery SHOULD recommend revoking them and + rotating active keys. Before completion, durably publish the replacement + device envelope and the new recovery bundle and confirm they decrypt. + + A trusted device can replace a lost recovery kit by installing a new + recovery authority and re-encrypting the recoverable keyring. It cannot + display the old recovery secret if that secret was not retained. + + Treat a possibly exposed recovery key as a compromise: replacing its + envelope alone is insufficient. Rotate the recovery authority and + active content keys. Previously copied ciphertext may remain readable. + + Losing every approved device and the recovery kit is unrecoverable by + the service. Offer creation of a separate empty vault only through an + explicit flow; do not automatically delete or overwrite the old vault. + A recovery key restores access to existing ciphertext, not deleted data. + + +7. Cryptographic Records and Protocol Requirements +-------------------------------------------------- + +7.1. Candidate suite and library policy + + The proposed baseline is AES-256-GCM for authenticated content + encryption, HKDF-SHA-256 for domain separation, Ed25519 for device and + policy signatures, and HPKE for recipient key envelopes. The candidate + HPKE suite uses X25519, HKDF-SHA-256, and AES-256-GCM. These are design + choices to validate across Rust and Swift, not new implementations of + the primitives. HPKE base mode requires the separate authorization + signature; encryption to a public key does not identify the sender. + + No direct E2EE dependency is selected by this RFC. Select maintained, + reviewed implementations; check provenance, release age, install/build + behavior, and source before installing unfamiliar dependencies. + Native platform facilities may be used behind the same tested format. + Do not implement cryptographic primitives or ad hoc key exchange. + +7.2. Logical content envelope + + +------------------------------------------------------------+ + | version / suite / vault ID / generation / key epoch | + | object ID / kind / writer ID / batch or revision ID | + | nonce / ciphertext / tag / author proof | + +------------------------------------------------------------+ + + All interpretation-relevant headers MUST be authenticated. Author + proofs establish which approved device produced a write; possession + of a group key alone is insufficient for writer attribution. They bind + the complete envelope and the applicable membership/write epoch. + + Associated data binds the object to its vault, generation, purpose, + writer, and immutable batch/revision identity. Registry records also + bind the row, field, operation semantics, and original HLC. Checkpoint + records bind the covered sequence and frontier/manifest identity. + + A chat row's server-assigned sequence is unknown at encryption time. + Do not pretend it is authenticated by the original writer. It remains + transport ordering metadata; checkpoint manifests and local monotonic + state provide additional checks but not proof of server completeness. + + Freeze one deterministic signed/AAD encoding before implementation. + Specify field lengths, integer ranges, byte order, base64 variant where + needed, signature inputs, and duplicate/unknown-field handling. Ordinary + JSON reserialization MUST NOT determine a signature's meaning. + +7.3. Nonces, persistence, and retries + + Every encryption key/nonce pair MUST be unique, including after process + restart, restored device backup, concurrent writers, and key rotation. + The nonce/subkey allocation scheme and per-key usage limits are blocking + protocol-review items. A process-local counter or shared group counter + without a crash/clone-safe allocation design is not sufficient. + + Encrypt once and durably store the immutable envelope plus batch ID + before sending. Retries use identical bytes. On an epoch change, create + a new transport batch for newly encrypted bytes; never reuse an old + batch ID with different ciphertext. Preserve semantic command IDs. + + Include authentication/signature overhead in row, frame, header, and + registry budgets. Reject oversized data before placing it in a retry + queue. Bound allocations before parsing or decrypting untrusted input. + Unknown formats or suites MUST fail closed, not invoke a legacy parser. + +7.4. Blob and checkpoint revisions + + Mutable sidecars and checkpoint replacements receive immutable content + revisions. A signed manifest binds revision, ciphertext digest, size, + key identity, and relevant sequence/frontier metadata. Object digests + exposed to the backend are ciphertext digests, not plaintext hashes. + + A small checkpoint MAY use one AEAD envelope: resume ciphertext bytes + with HTTP Range, then authenticate the full object before Loro import. + Replacing a checkpoint at the same covered sequence still changes its + revision; sequence number alone is not a safe resume validator. + + If chunked encryption is required, use a reviewed construction that + authenticates chunk index, object revision, and total/final length. + Reject reordering, truncation, duplication, and cross-object splicing. + Do not assemble unverified plaintext into a trusted file or document. + +7.5. Signed-wrapper v1 fixture profile + + This subsection fixes a candidate signed-wrapper encoding for isolated + conformance tests. It does not close G1-G3, authorize transport use, or + define the encrypted payload, its nonce allocator, or membership rules. + The wrapper is shared by public control records and encrypted payloads; + a valid wrapper signature is NOT proof that its payload is encrypted. + + Encode exactly one RFC 8949 core-deterministic CBOR map, with these + integer keys in ascending order. All lengths/unsigned integers use + their shortest encoding. Reject duplicates, missing/unknown/reordered + keys, indefinite lengths, tags, text, floats, negative integers, and + trailing bytes. There is no alternate JSON or legacy interpretation. + + Key Field Type + 0 wrapper version unsigned integer, exactly 1 + 1 record kind unsigned: 1 policy, 2 key envelope, 3 content + 2 vault ID byte string, 16 bytes + 3 storage generation byte string, 16 bytes + 4 authorizing epoch unsigned integer, 1 through 2^64-1 + 5 object ID byte string, 16 bytes + 6 author device ID byte string, 16 bytes + 7 immutable revision byte string, 16 bytes + 8 membership hash byte string, 32 bytes + 9 payload bounded byte string + 10 Ed25519 signature byte string, 64 bytes + + IDs are raw opaque bytes, not UUID text, account names, or paths. The + membership hash identifies the policy authorizing the signer, not the + hash of this record; genesis cannot require its own hash. Genesis and + membership transitions need their separate reviewed payload/trust rules. + A zero hash is not permission to bypass trust verification. + + Signature input is the ASCII bytes "zeron/signed-record/v1" followed + by one zero byte, then the deterministic CBOR map of keys 0 through 9 + (map length 10, not the length-11 signed map). All payload bytes are + covered. Use ordinary Ed25519, not Ed25519ph. Signature output itself + need not be deterministic across platforms; persist the complete signed + record once and retransmit those same bytes. + + Parsing yields an UNVERIFIED record. Verification requires an expected + binding obtained from trusted local profile/object/policy state: kind, + vault, generation, epoch, object, author, and membership hash. The caller + separately supplies that author's public key from verified membership; + no public key is embedded in this wrapper. Comparing a record with a + context copied from that same untrusted record is not authorization. + Only successful context AND signature verification yields payload access + through the verified-record interface. This still does not authorize + commands, validate a membership transition, or prove freshness. + + The revision is signed but not a server sequence. Dedupe/replay and + accepted-history policy remain separate. Valid signatures do not allow + old epochs to be treated as new writes after revocation. + + Each caller supplies a trusted payload byte limit. Validate encoded + size (payload limit plus at most 256 bytes of wrapper overhead), field + lengths, and declared payload length before copying or signing data. + Limit arithmetic must not wrap. Empty payloads are structurally valid; + each reviewed payload schema decides whether they are meaningful. + Full-width epoch values must not be rounded through JavaScript numbers. + + The isolated codecs implement only this fixed map grammar, not a + general CBOR value tree. Payload codecs, cipher-suite identifiers, + AEAD associated data, key IDs, nonce/salt fields, and author admission + checks remain required before encrypted records can enter transport. + +7.6. Ed25519 input-encoding prechecks + + The isolated verification adapters reject small-order and noncanonical + encodings before invoking the existing Ed25519 implementation. This + is a candidate verification-policy restriction, not complete device-key + admission and not a new signature algorithm. + + For the 32-byte public key A and the first 32 bytes R of a signature: + + - Inspect the little-endian y-coordinate after masking the sign bit + in a COPY of the final byte. Require y < p, where p = 2^255 - 19. + - Reject y equal to 0, 1, p-1, or either of these two little-endian + order-eight coordinates (both sign-bit variants are rejected): + + 26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05 + c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a + + For the final 32 bytes S, require the full little-endian integer to be + less than L = 2^252 + 27742317777372353535851937790883648493. Do not mask + S's high bits or reduce it modulo L. S = 0 passes this encoding check + but still requires a valid signature equation under an acceptable key. + + Pass the ORIGINAL, unmodified A/R/S bytes to the signature library. + These comparisons inspect public data only. Signature failures retain + the existing coarse authentication error and do not yield a verified + record. Future transport callers MUST NOT turn such failures into cursor + advancement or an alternate verification mode. + + The identity-point regression is concrete: the underlying libraries + tested here accepted A = R = identity, S = 0 for a synthetic message. + No valid device private key is needed for that construction. It is not + a break of signatures under correctly generated keys, but it cannot be + used as device-possession evidence. The adapter must reject it even if + a caller accidentally supplies the identity point as its trusted key. + + Passing the encoding precheck does NOT prove that bytes decode to an + acceptable prime-order device key. The existing library still performs + signature verification; mixed-order point policy, complete admission, + and cross-version agreement remain G1/G2 review requirements. Do not + enroll a device based on the precheck alone. These rules apply only to + Ed25519, not to X25519/HPKE keys. + +7.7. Encrypted content record implementation + + Content uses the signed wrapper with kind = 3. Its payload is one + deterministic CBOR map with exactly these ordered integer keys: + + 0 format version unsigned, exactly 1 + 1 suite unsigned, exactly 1 (AES-256-GCM/HKDF-SHA256) + 2 purpose unsigned: 1 update, 2 checkpoint, 3 frontier, + 4 registry field, 5 tail, 6 diff, 7 blob, + 8 device sidecar, 9 registry lifecycle + 3 content-key ID byte string, 16 bytes + 4 per-record salt byte string, 32 bytes + 5 ciphertext and tag byte string; final 16 bytes are the GCM tag + + A content root key is random, 32 bytes, and scoped to one vault, storage + generation, write epoch, and object. Its ID and scope are immutable in + the key object. A new epoch requires fresh root keys, not relabeling or + rewrapping the old roots. Signers likewise bind a device ID to their + private signing key; a caller cannot label a seal as another author. + + Define C as the deterministic CBOR map of signed-wrapper fields 0..8 + (length 9, including the fresh revision). Define H as the deterministic + CBOR map of encrypted-payload fields 0..4 (length 5). Then: + + K = HKDF-SHA256(root key, salt, + ASCII("zeron/content/key/v1") || 00 || C || H, 32 bytes) + AAD = ASCII("zeron/content/aad/v1") || 00 || C || H + + Use K for exactly one AES-256-GCM encryption with a 12-byte zero nonce. + Generate the 16-byte revision and 32-byte salt from fresh OS randomness + for each new seal. Randomness failure yields no record. The public seal + API accepts no caller-provided nonce or salt. The complete encrypted + payload is then signed using Section 7.5, and the resulting immutable + record is persisted before transport. Retries use those stored bytes; + re-encryption always creates a fresh salt and transport revision while + preserving semantic command IDs inside the plaintext. + + Opening first verifies the expected wrapper context and author, then + requires the expected purpose, key ID, and root-key scope. It derives K + and authenticates GCM before returning any plaintext. Unknown formats, + suites, and purposes fail closed. Signature-only or plaintext draft + records are not a content-encryption fallback. + + The implementation caps plaintext at 16 MiB minus 400 bytes, with a + smaller caller-supplied limit for each transport/object type. The 400 + bytes conservatively cover encryption and signed-wrapper overhead; + existing row and sidecar quotas still apply. Length checks precede + randomness, copying, and crypto work where possible. + + This selects the per-record-derived-key approach for implementation, + not a claim that review is finished. Entropy quality, aggregate key-use + budgets, collision/authentication bounds, restored-device identity, and + live VM/RNG-state cloning remain explicit release-review requirements. + No compatibility with earlier unshipped draft encryption is required. + + +7.8. Control-plane records implemented in this PR + + Policy records use the signed wrapper with kind = 1, object ID = sixteen + zero bytes, epoch = the record's active epoch, and membership hash = the + PARENT record's hash (genesis: zeros). The payload is one deterministic + CBOR map: + + 0 version (1) 1 sequence 2 parent hash (32) 3 profile hash (32) + 4 epoch 5 operation: 1 genesis, 2 add device, 3 revoke device, + 4 rotate recovery, 5 recovery transition + 6 recovery signing key (32) 7 recovery encryption key (32) + 8 devices: array of [id 16, signing 32, encryption 32, status 0|1] + + Membership hash = SHA-256("zeron/membership/v1" 00 || record bytes). + Profile hash = SHA-256("zeron/profile/v1" 00 || org || 00 || user). + Recovery authority ID = first 16 bytes of SHA-256("zeron/recovery-id/v1" + 00 || recovery signing key). Add keeps the epoch; every other operation + increments it. Existing devices keep their index; a revoked device is + never revived; recovery keys change both-or-neither. + + Key envelopes use kind = 2 with payload {0 version, 1 recipient kind + (1 device, 2 recovery, 3 epoch), 2 recipient id (16), 3 encapsulation or + salt (32), 4 ciphertext}. Keyring envelopes are HPKE base mode + (DHKEM X25519/HKDF-SHA256, AES-256-GCM) with info = + "zeron/keyring-envelope/v1" 00 || C || H(0..2). Object keys are wrapped + under an epoch key with K = HKDF(epoch key, salt, "zeron/object-key/v1" + 00 || C || H(0..3)), AAD = "zeron/object-key/aad/v1" 00 || C || H(0..3), + one zero-nonce AES-256-GCM over id || key. The keyring is + {0: 1, 1: [[epoch, key32] ...]} with ascending epochs. + + Enrollment proofs sign "zeron/enroll/v1" 00 || vault || request || device + || signing key || encryption key. The pairing code is the first 32 bits of + SHA-256("zeron/pairing-code/v1" 00 || those fields || genesis hash) mod + 10^8, shown as NNNN-NNNN; binding the genesis hash lets the user detect a + relay that presents a substitute vault to the pending device. + + Registry field values (Section 9) are content records with purpose 4 + under the object "registry"; the authenticated plaintext is the JSON + object {kind, id, field, hlc, value} and MUST equal the slot the server + filed the value under. value = null is an authenticated field deletion. + The wire value is {"e1": base64(record)}. Encrypted profiles use the + registry generation /registry/{org}/e1 (room reg1e1/{org}/{user}). + + Row lifecycle proofs are content records with purpose 9 under the same + object; the authenticated plaintext is {kind, id, op: "delete", hlc}. + A delete op in an encrypted generation MUST carry one as `proof`; the + room stores it as the tombstone's `delProof`, serves it with the row, + replaces it only with a causally newer delete, and drops it on + revival. Readers accept a tombstone only with a verified proof whose + row and clock equal the slot the server filed it under and whose clock + is newer than the verified baseline (max field clock, or the previous + tombstone's clock); a live row over a verified tombstone is accepted + only with a verified field newer than the tombstone. Anything else is + dropped, never applied: a relay can neither delete nor resurrect a + row. Tombstone compaction (30 days) keeps the server-side gcFloor rule; + a reader that missed a compacted tombstone resyncs and sees absence, + never a fabricated deletion. + + Object IDs are the first 16 bytes of SHA-256("zeron/object-id/v1" 00 || + kind || 00 || id). Encrypted profiles publish chats to a separate room + (chat2/{id}-e1) whose local cursor carries doc epoch 3; the plaintext room + is retained, never silently deleted (Section 12.3). + + +8. Chat Log, Checkpoint, and Cursor Integration +---------------------------------------------- + + Keep ChatRoom an opaque log. No server decryption, Loro replay, or + materialization is introduced. Content encryption sits above both the + WebSocket and HTTPS transports, not in only one send function. + + Local Loro export -> seal -> durable outbox -> WS or HTTPS + WS or HTTPS -> validate -> decrypt -> Loro import -> persist + + Cover every path: new rows, duplicate sends, HTTP fallback, backfill, + initial seeds, threshold checkpoints, reset recovery, and rejected-row + recovery. Parent and live subagent documents use the same boundary. + Clear ping/probe frames may describe transport liveness, not content. + + The checkpoint frontier is independently encrypted/authenticated and + bound to the snapshot's manifest. A frontier that cannot be verified + MUST NOT be treated as "already contained" or as an empty checkpoint. + Decrypt it before planning catch-up. Missing keys are distinguishable + from a valid frontier whose state is absent locally. + + Before materialization, authenticate the writer and applicable epoch, + verify the envelope, and decrypt. Then apply the existing document + semantics. A valid signature does not make arbitrary imported commands + safe: the existing host ownership and command authorization rules stay. + + Cursor handling requires an explicit result, rather than the current + void apply_row path [R2], for at least these cases: + + Applied and durable Doc and cursor committed atomically. + Key unavailable Keep data, refresh keys, do not advance. + Authentication failed Pause/quarantine, do not advance. + Unsupported version Require upgrade, do not advance. + Import/storage failed Preserve last good state, do not advance. + + Authentication failure MUST NOT take the current "skip malformed row + and persist cursor" branch. Acknowledging a local push also MUST NOT + advance the verified receive cursor over an unauthenticated gap. + Checkpoint pruning must never cover unverified/unpersisted content. + + If causal imports remain pending, persistence must retain what is needed + to resume them; advancing a cursor over state that disappears on export + is not acceptable. Keep the existing gap-repair discipline and test it + with encrypted data, not only successful single-row imports. + + Bad ciphertext must not cause a rapid reconnect loop. Preserve bounded + diagnostic identity, retain last good local state, and require a valid + recovery path. The relay may deny availability; clients must not hide + this by deleting data or declaring themselves caught up. + + +9. Registry Encryption Without Changing Merge Semantics +------------------------------------------------------- + + RegistryRoom is a per-field HLC/LWW table, not an opaque document [R4]. + Encrypting an entire row into one field would cause concurrent changes + to different fields to overwrite each other. Preserve its current + merge granularity instead. + + The encrypted wire table retains row kind, random row ID, original + field names, per-field clocks, and row lifecycle markers. Values such + as title, cwd, branch, preview, config, device name, and checkout data + are encrypted. Any plaintext field-value exception requires a named + addition to the Section 2 disclosure; do not rely on a denylist of + today's sensitive fields. New application fields encrypt by default. + + Each winning field retains a self-contained authenticated value and + author proof bound to its row, field, clock, and operation semantics. + A server-merged row contains contributions from several writers: do + not require one writer signature over the final server-assembled row. + + Field deletion is an authenticated field tombstone, not an unproven + missing value. Row delete/create/revival records also retain proof. + The current merge code clears values and clocks on row deletion; the + encrypted protocol MUST preserve sufficient lifecycle evidence for a + new client to validate deletion/revival after compaction or full sync. + This needs a versioned registry schema, not just encrypt(value). + + A null application value, field deletion, and an absent field must have + unambiguous representations matching existing semantics. Reseeding + carries original clocks and proofs; it does not impersonate an old + author or promote stale values by inventing newer clocks. + + Keep authenticated wire state separate from decrypted projection and + pending local edits. Verify every received field/lifecycle record + before updating authoritative local state or its committed cursor. + An invalid field cannot poison the HLC and make all future writes lose. + + Encryption/key rotation is not a semantic edit. Preserve original + conflict clocks during re-encryption. Use a signed generation cutover + or a separately specified rekey operation; do not fight the strict + newer-clock rule by assigning arbitrary new application timestamps. + + Retain tombstone GC and full-repair behavior only with the required + proofs. A server can still omit an entire valid row; signatures do not + prove absence. Do not interpret an unauthenticated "full" response as + permission to destroy known local state or lower pinned trust policy. + + Sidebar search, sorting, and previews remain local after decryption. + Lost server-side plaintext inspection is an intended consequence. + + +10. Device RPC, Attachments, and Other Content Paths +--------------------------------------------------- + + DeviceRoom can retain outer routing fields while forwarding an + authenticated encrypted channel [R7]. The channel binds the vault, + device identities, negotiated protocol, and a fresh session transcript. + Use a reviewed authenticated channel with ephemeral session keys, + directional keys, replay protection, and explicit rekey limits. + + The implemented profile is Noise_XX_25519_AESGCM_SHA256 (Noise revision + 34), one fixed profile, no negotiation. Each device's Noise static is + the X25519 encryption key its membership entry publishes, so the + transcript authenticates the same identity the policy chain vouches + for. The prologue is "zeron/device-channel/v1\0" || vault id || + storage generation; message 2 and message 3 carry the sender's device + id inside the encrypted handshake payload. A side accepts its peer + only when the pinned membership head lists an ACTIVE device with that + id AND that static key, and the peer is not itself. The initiator + checks the responder before sending message 3 (it reveals nothing but + an ephemeral key to a host it does not trust). After the handshake the + session uses Noise transport mode: per-direction keys and nonces + (replay-rejecting), application frames chunked below Noise's 64 KiB + limit with a continuation flag, an 8 MiB frame cap, and a budget of + 2^32 messages per direction after which the session is retired. + Membership is re-checked on every inbound frame, so a revocation that + has reached a device ends the session at the next byte. The relay + carries kind "chan" frames only; for an enrolled profile it closes any + socket that sends a plaintext RPC frame, and the host refuses such a + frame independently. There is no plaintext fallback in either + direction. + + A relay-assigned connection ID is not a cryptographic device identity. + Authenticate the peer and membership before exposing serve_connection + or executing RPC. Scope each channel to one profile; the current + installation-wide device ID must not authorize another profile. + A claimed transport echo or backend control frame cannot authorize a + command, unlock keys, or establish end-to-end peer health. + + Protect the entire application payload, including RPC method/arguments, + replies, streams, terminal bytes, directory names, file chunks, and + errors that contain workspace data. This covers the current host-local + attachment path without adding R2 mirroring [R6]. Existing path jails, + command deduplication, and method authorization remain necessary. + + Local-only key-management operations MUST NOT become callable merely + because the existing RPC surface is forwarded through an encrypted + channel. Do not expose device private keys or recovery material through + diagnostic/export methods. + + Tail, diff, and device sidecar publishers use typed encrypted codecs. + The legacy working-tree diff route must migrate to a ciphertext-capable + endpoint or be disabled for encrypted profiles. The same rule applies + to readers: no legacy fallback fetch when decryption fails. + + R2 tool blobs include finished subagent transcripts [R5]. Use opaque + object IDs and encrypted descriptors for filenames, media types, source + paths, and plaintext checksums. Serve ciphertext as octet-stream where + appropriate. No public plaintext-content-addressed deduplication. + + A nudge may name only an opaque room/device route. It is a wake-up hint, + never authority to execute. The host fetches and verifies the queued + command. Rich presence, future notification previews, exports, and new + attachment mirrors must pass the same content-path review before use. + + +11. Revocation and Key Rotation +------------------------------- + + Removing an envelope is not revocation: a device may already know the + workspace and content keys. A complete transition is: + + 1. Authorize a signed membership change from a trusted device. + 2. Prepare a fresh random write epoch and fresh active content keys. + 3. Publish key envelopes for retained devices and recovery. + 4. Fence old-epoch writers and close revoked device sessions. + 5. Activate the new epoch; confirm retained devices can catch up. + + Publication and activation need an explicit recoverable transaction + protocol. A crash must not activate keys that no retained device or + recovery bundle can obtain. Competing rotations use the membership + compare-and-swap rule, not independent last-write-wins updates. + + Rotate keys used for new chat updates, checkpoints, registry values, + blobs, and live channels. Rewrapping the same content keys under a new + vault key is insufficient. Dormant objects may keep historical keys + for reading, but their first subsequent write must use a fresh active + key. New full snapshots use new keys even if they contain old content. + + Honest server enforcement rejects old-epoch new writes, including from + stale clients. Reconnecting retained clients first obtain and verify + the active policy, then re-encrypt queued local work. Do not upload an + old outbox first and refresh membership afterward. Preserve semantic + command IDs to avoid executing the same intent twice. + + Historical old-epoch records remain readable with their original + proofs. The protocol must distinguish accepted history from a new + submission claiming to be history; a writer timestamp is not proof of + acceptance before revocation. Epoch closure/frontier rules are a + blocking review item, including checkpoint replacement and restore. + + Offline devices learn of revocation on reconnect. A malicious server + can withhold the transition from a stale device; v1 does not promise + immediate global revocation or perfect freshness under partition. + Once a client learns a new policy it MUST reject rollback. If a future + requirement demands bounded freshness, it needs a separate availability + trade-off rather than an undocumented timeout. + + Required removal copy: + + "This stops future sync access after the change takes effect. + It cannot erase information the device already downloaded." + + +12. Migration, Compatibility, and Old Plaintext Copies +------------------------------------------------------ + +12.1. State and ordering + + LEGACY -> PREPARING -> WRITE-FENCED -> COPYING -> VERIFYING + | + v + ENCRYPTED / LEGACY RETAINED + | + explicit cleanup + | + v + ENCRYPTED / RETENTION STATUS + + Migration is per profile, resumable, and explicitly initiated. Its + encrypted manifest records source objects, revisions, progress, and + failures without publishing sensitive labels in cleartext. + + A. Preflight. Inventory sessions, subagent docs, registry rows, + sidecars, blobs, local pending work, and available host copies. + Require a saved recovery kit and upgrade/stop every known legacy + content producer, including offline devices before their next + use. Obtain explicit confirmation for unavailable devices; do + not equate a server block with stopping their plaintext requests. + Missing sources block completion or require an explicit exclusion + recorded for the user; never silently drop history. + + B. Quiesce and fence. Pause/drain content producers, close old + sockets, and reject new legacy writes for the profile. Record + final source revisions after in-flight writes are fenced. + Offline clients keep local work until they upgrade/rejoin. + + C. Copy. Create fresh encrypted storage generations and seed them + from the final source state. Preserve Loro lineage, semantic + chat/command IDs, and host processed-command ledgers. Maintain + encrypted mappings to new random transport IDs. Encryption + alone is not a reason to rebuild or replay command history. + + D. Verify. Check decryptability, source coverage, pending-work + continuity, and recovery-bundle completeness. A second approved + device SHOULD test restore; the originating device MUST also + test the actual saved recovery kit/bundle before activation. + + E. Activate. Publish and pin the signed encrypted-only policy after + target data and envelopes are durable. Resume only encrypted + writers. Show retained legacy-copy status separately. + + F. Clean up. Offer a separately confirmed cleanup operation after + validation and the agreed retention decision. Keep a record of + what was removed, what remains, and what cannot be verified. + + Local pending commands may be created while offline/migrating but must + not leak into old transports. No plaintext/ciphertext dual-write. + Source snapshots are retained until validation and explicit cleanup. + A crash at any stage resumes from durable progress, not from guesswork. + +12.2. Server enforcement and downgrade resistance + + New ciphertext namespaces alone are not sufficient: an old client may + continue uploading plaintext to its old room, blob, or diff endpoint. + The Worker and DO paths need profile-scoped legacy-write fences and + capability checks covering existing sockets as well as new requests. + + A rejection prevents accepted writes, not disclosure of request bytes + already sent to the backend. Even a revoked bearer can accompany a + plaintext body before rejection. Activation therefore requires the + client upgrade/stop precondition in 12.1; it cannot be guaranteed solely + by server code. Display the risk of restarting an obsolete client. + Compatible clients must check their local gate before serializing any + content and refresh policy before resuming content transport. + + Some current routes identify only chat/device/user, not the full + profile. The implementation must establish authoritative ownership and + migration mappings before fencing; it must not trust a caller's new + profile label or disable unrelated profiles belonging to the user. + Unknown legacy room creation for an activated profile must be denied. + Route inventory and coverage are a release gate, not follow-up work. + + Client version strings alone are not proof of encryption or device + identity. Enforce protocol framing and device possession, and retain + client-side encrypted-only policy pins. A malicious authorized device + can intentionally disclose plaintext; that is outside this promise. + + Every supported downgrade target must honor encrypted local-profile + markers and stop with an upgrade-required error rather than interpret + missing data as a fresh legacy profile. A marker cannot retrofit checks + into already shipped binaries: those versions are unsupported and must + stay stopped until upgraded. Define the safe local-layout transition + and downgrade range explicitly. Losing local pins or an empty bootstrap + response must not auto-create a replacement vault over existing data. + + Before fencing, setup can be canceled without changing legacy sync. + After fencing, pause/resume is safe; abandoning the transition requires + an explicit reviewed path. After activation, rollback means restoring + an earlier compatible encrypted client/state, never plaintext sync. + +12.3. Historical copies + + Re-encrypting data does not undo previous exposure. Cleanup inventory + must include active and legacy DO namespaces, tails/diffs, R2 blobs, + chat and registry backups, historical attachment objects, any configured + provider recovery snapshots, and operational exports/log retention. + Inspect actual deployment retention; do not invent a deletion deadline. + + Nightly backup sequencing must be generation-aware. The existing + monotonic-sequence guard must not cause an older plaintext backup to + remain the apparent current backup after an encrypted generation starts + at a lower sequence. Ciphertext backup manifests retain key references. + + A cleanup receipt cannot prove an operator never copied plaintext. + Do not label data "never exposed" or promise cryptographic erasure of + prior plaintext. Cleanup of cloud data is separate from local cache + cleanup and is not performed by this RFC. + + +13. Local Storage, Observability, and Operational Limits +------------------------------------------------------- + + Use platform secure storage for device keys with deliberate access and + backup attributes. On Apple platforms, do not blindly reuse the auth + token helper: encryption enrollment requires checked persistence errors + and an explicit device-only versus restorable-key policy [R10]. + Device identity must not be silently cloned through OS backup/restore. + + Existing SQLite snapshots, run journals, staged attachments, and UI + caches may contain plaintext on approved endpoints. This RFC does not + retrofit full local-at-rest encryption. Document that boundary and keep + profile isolation, file permissions, and OS disk protection intact. + Do not imply the vault is locked if readable local data remains exposed + under an independent application-cache policy. + + Device approval is not complete until key storage is durable. Handle + secure-store denial, full disk, interrupted writes, and restored backups + without generating a second untracked identity or losing the keyring. + Avoid plaintext key material in crash dumps and diagnostics as far as + supported by platform/library facilities; never claim perfect memory + erasure on a running endpoint. + + Observability may include coarse error codes, protocol versions, random + identifiers, queue lengths, ciphertext sizes, and key-epoch numbers. + These remain metadata with a retention/access policy, not public data. + MUST NOT log secrets, cleartext bodies, decrypted previews, paths, + enrollment secrets, recovery keys, or authorization-bearing URLs. + Error formatting from parsers/HTTP clients needs the same review. + + Support can diagnose transport, authorization, and ciphertext integrity + without obtaining user keys. Any user-exported diagnostic package is + locally reviewed/redacted and explicitly consented; no automatic upload + of plaintext under the label "debugging". + + Server-assisted search or previews over plaintext are unavailable. + Notifications, if added, must use generic text or device-side decryption. + Backup restore works with ciphertext plus the appropriate keys; the + backend can restore bytes without gaining the ability to inspect them. + + +14. Implementation Sequence and Acceptance Tests +------------------------------------------------ + +14.1. Workstreams + + Phase 1 This RFC, threat/UX review, and resolution of protocol gates. + No production changes. Record decisions before coding them. + + Phase 2 Implement the reviewed record codecs and Rust/Swift vectors; + device secure storage, signed vault/membership records, + bootstrap service, pairing, recovery, and native/CLI states. + + Phase 3 Chat transport and frontier/checkpoint integration using those + codecs; durable outbox and explicit verified-cursor outcomes. + + Phase 4 Registry field/lifecycle proofs, device channels, attachments, + tails/diffs, R2/subagent blobs, and diagnostics review. + + Phase 5 Capability fences, migration journal, generation-aware backups, + old-client handling, and consented legacy-copy cleanup tooling. + + Phase 6 End-to-end security/reliability review, opt-in rollout, and + operational recovery exercises with ciphertext-only storage. + + The user-visible feature switch stays unavailable until all content + paths and migration requirements are satisfied. Workstreams may be + separate PRs; a partially encrypted deployment is not a public beta + claiming full workspace protection. + +14.2. Acceptance matrix + + Test group Required result + ------------------------- ------------------------------------------- + Cross-language vectors Rust and Swift agree on key envelopes, + signatures, AAD, failures, and wire bytes. + + Untrusted backend Account credentials plus DO/R2/bootstrap + dumps cannot decrypt data or enroll a peer + without the required cryptographic proof. + + Pairing attacks Substituted keys, wrong vault, mismatched + code, expired/replayed request, and absent + user approval all fail without enrollment. + + Recovery Recover without an old device, including + after multiple key rotations. Test a lost + kit, replaced kit, stale bundle, and failed + persistence. No auth-only recovery path. + + Ciphertext validation Wrong key, modified header/body/tag/proof, + cross-chat/field substitution, truncation, + and unsupported version never materialize. + + Nonce/retry safety Crash, concurrent writers, cloned/restored + storage, resend, and rekey cannot reuse a + key/nonce for different plaintext. + + Cursor durability Missing keys, invalid proof, disk failure, + pending causal ops, and interleaved own ACK + do not advance over uncommitted content. + + Transport parity WS and HTTPS fallback support the same + security checks, offline outbox, dedupe, + reconnect, and bounded failure handling. + + Checkpoint resume Replacement at equal covered sequence, + changed revision, and mixed byte ranges + cannot produce accepted spliced plaintext. + + Registry convergence Concurrent different-field edits survive; + null/delete/revival, GC, reseed, rekey, and + original clocks match existing semantics. + Invalid proofs cannot poison valid state. + + Device channel Wrong peer/profile, replayed handshake or + command, relay-forged health, and revoked + membership cannot authorize application RPC. + + Revocation Rotate active keys, reject new old-epoch + writes, retain valid history, and catch up + retained offline devices without duplicate + commands. Test interrupted/concurrent changes. + + Migration Resume every interrupted stage; preserve + lineage, IDs, pending work, and host ledgers. + Compatible clients emit no plaintext; legacy + writes are not accepted. Test upgrade/stop + preflight, not just server rejection. + Missing sources cannot look like completion. + + Plaintext canaries Seed distinctive titles, paths, branch names, + file/terminal content, attachments, and + subagent output. Capture request payloads + after sealing, DO/R2 state, sidecars, and + diagnostics; none contain the cleartext. + Decode representations before scanning; + base64 is not evidence of encryption. + + Backup and cleanup Restore encrypted data using recovery keys; + verify generation selection and old-copy + inventory/retention reporting. A cleanup + test cannot claim deletion of unknown copies. + + UI and platform failures No empty-success transcript, implicit + downgrade, unapproved auto-enrollment, or + lost draft on lock/sign-out/secure-store + failure. Headless restart behavior matches + the chosen credential mode. + + Extend existing Rust sync/engine/RPC tests, Swift sync tests, and edge + unit/workerd tests [R11]. A crypto unit test alone does not demonstrate + end-to-end coverage. Include a two-native-device plus headless scenario + and a restore performed without backend access to any private key. + + +15. Blocking Review Decisions and Alternatives +---------------------------------------------- + + The product decisions in Section 1 are proposed for acceptance now. + The following technical gates need explicit decisions and test vectors, + not silent choices scattered across later PRs: + + G1. Exact pairing/channel construction, human verification flow, and + audited Rust/Swift implementations. Freeze one versioned suite; + do not negotiate a weaker legacy mode. + + G2. Canonical record/signature encoding, domain-separation labels, + nonce/subkey allocation with restart/clone safety, usage limits, + and immutable checkpoint/chunk manifests. + + G3. Membership history retention, epoch closure/frontiers, recovery + authority transitions, and the distinction between accepted old + history and a new old-epoch forgery after revocation. + + G4. Registry lifecycle proof schema, tombstone compaction, original-clock + reseeding, and rekey behavior without changing conflict semantics. + + G5. Secure-store adapters and backup/restore policy on macOS, iOS, and + Linux/headless hosts; enforceable old-client local-schema behavior. + + G6. Profile ownership for legacy routes, in-flight write fencing, + migration cancellation/recovery, and measured retention of old + cloud copies. These need deployment verification, not assumptions. + + Implementation direction accepted by the maintainer on 2026-09-04: + + - Native CryptoKit content/key-envelope operations on Swift, a small + reusable Rust crypto boundary, and a shared Rust channel through + a narrow Swift bridge rather than a second handshake implementation. + - Device-bound, non-syncing iOS Keychain keys accessible after the + first unlock following boot; replacement devices approve/recover. + - Explicit opt-in unattended headless operation using supported + protected service credentials, with unlock-on-boot also available + and no silent plaintext-file fallback. + - A grouped, checksummed Base32 recovery secret and a locally saved + recovery file carrying vault/trust-anchor metadata. + + These choices select implementation/UX directions, not exact wire + encodings, nonce rules, libraries, or closure of the gates above. + + Before broad release, an independent security review SHOULD cover the + complete construction and its implementation, particularly pairing, + recovery, revocation, and the command-bearing device channel. Review + findings and unresolved limitations must be tracked as release gates. + + Alternatives considered: + + Blob-only encryption Rejected as the feature boundary: tails, + diffs, registry, and RPC still expose data. + + Whole-registry blob Rejected for v1: changes independent-field + merging or recreates a client-log design. + + Login-derived keys Rejected: account recovery/compromise becomes + decryption authority; password/SSO coupling. + + One permanent key Rejected: device removal cannot protect + future writes and key usage is unbounded. + + Server/admin escrow Rejected: does not meet backend-confidentiality + goal. An enterprise escrow product, if ever + added, needs different explicit claims. + + Per-space grants Deferred: useful for restricted VPS runners, + but requires finer key and RPC authorization + boundaries than the current full-peer model. + + Passkey-only recovery Deferred: may improve UX later, but requires + a separately reviewed cross-platform key + derivation and recovery story, not merely a + successful WebAuthn login. + + Local-at-rest vault Deferred as a separate project. Protecting + transport/cloud copies does not encrypt + existing endpoint files and run journals. + + +16. Repository References +-------------------------- + + Paths are repository-relative. Function/type names are preferred over + line numbers so this memo survives nearby edits. Baseline: a0650ab. + + [R1] Opaque chat storage, frame codec, backup and HTTP surfaces: + edge/src/chat-log.ts + edge/src/chat-room.ts + edge/src/chat-frames.ts + + [R2] Rust chat transport, sink, persistence, and checkpoint fetch: + crates/sync/src/chat_client.rs + crates/sync/src/store.rs + crates/engine/src/chat2_host.rs + Types: ChatClient, ChatDocSink, EngineChatSink, + EdgeCheckpointFetcher, EdgeChatTransport. + + [R3] Seeds, tail/checkpoint publishing, and blob upload/read: + crates/engine/src/doc_host.rs + Functions: chat2_maintenance, spawn_chat2_checkpoint, + upload_tool_sidecar, fetch_tool_blob. + + [R4] Registry field merge, authoritative state, and transport: + edge/src/registry-core.ts + edge/src/registry-room.ts + crates/doc/src/registry.rs + crates/sync/src/registry.rs + crates/engine/src/workspace_host.rs + docs/registry-sync.md + + [R5] Legacy diff and finished subagent blob publication: + crates/engine/src/diff_sync.rs + crates/engine/src/sessions.rs + edge/src/index.ts + + [R6] Host-local attachments and device transfer: + crates/engine/src/uploads.rs + apps/ios/Zeron/Sync/SessionStore.swift + apps/ios/Zeron/Sync/UploadStash.swift + + [R7] Device routing, sidecars, and RPC channel boundary: + edge/src/device-room.ts + crates/rpc/src/device_room.rs + apps/ios/Zeron/Sync/DeviceRelayClient.swift + + [R8] Native Swift chat and registry clients: + apps/ios/Zeron/Sync/ChatRoomClient.swift + apps/ios/Zeron/Sync/RegistryClient.swift + apps/ios/Zeron/Sync/RegistryCore.swift + apps/ios/Zeron/Sync/DocDisk.swift + apps/ios/Zeron/Sync/WorkspaceStore.swift + + [R9] Profile, auth, and account-scoped storage: + crates/engine/src/profile.rs + crates/engine/src/auth.rs + ARCHITECTURE.md + + [R10] Existing keychain helper and desktop device settings: + apps/ios/Zeron/Auth/AuthClient.swift + crates/ui/src/settings/devices.rs + + [R11] Existing test infrastructure: + crates/sync/src/chat_client/tests.rs + crates/sync/tests/ + crates/engine/tests/ + crates/rpc/tests/device_room.rs + apps/ios/ZeronTests/ + edge/package.json + + [R12] Historical chat-log design and migration context: + docs/chat2-sync.md + Some status and sidecar notes are historical; consult code. + + + -- End of Draft 00 -- diff --git a/edge/package-lock.json b/edge/package-lock.json index 48161ba74..c0e5680c8 100644 --- a/edge/package-lock.json +++ b/edge/package-lock.json @@ -19,7 +19,7 @@ "loro-websocket": "^0.6.2", "typescript": "^6.0.3", "vitest": "^4.1.10", - "wrangler": "^4.111.0" + "wrangler": "4.113.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -66,6 +66,42 @@ "vitest": "^4.1.0" } }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": { + "version": "4.119.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.119.0.tgz", + "integrity": "sha512-ookClf+zly4DTc8pBMNrwGQzZKH8IpIYTXkjDw3XS7ZvBQ5mLYH6eOvfD5BEpk3U63zTbv91WRlo1UeRSKXa0g==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "5.20260801.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260801.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260801.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, "node_modules/@cloudflare/workerd-darwin-64": { "version": "1.20260801.1", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260801.1.tgz", @@ -2643,19 +2679,20 @@ } }, "node_modules/wrangler": { - "version": "4.119.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.119.0.tgz", - "integrity": "sha512-ookClf+zly4DTc8pBMNrwGQzZKH8IpIYTXkjDw3XS7ZvBQ5mLYH6eOvfD5BEpk3U63zTbv91WRlo1UeRSKXa0g==", + "version": "4.113.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.113.0.tgz", + "integrity": "sha512-ROGzSloJv0y21It6Oc9LaruNcu1tdiQ/XzL3Jc3YkFjzXEMXzTqVhA8vQaGMTdZHTjFP0PVcwAHNgaw3gXu4wA==", "dev": true, + "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20260801.0-alpha", + "miniflare": "4.20260721.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260801.1" + "workerd": "1.20260721.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2669,7 +2706,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260801.1" + "@cloudflare/workers-types": "^5.20260721.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -2677,6 +2714,728 @@ } } }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260721.1.tgz", + "integrity": "sha512-VivNMhiEdZIB4JBWxf1RMJGROErv53qmQ+dvhjA1evrCouvqRYW718VqDideU3PSV7Ythl5Df48NqZYWoaEHpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260721.1.tgz", + "integrity": "sha512-k7oye1ZiuwnnBBA2eTMduconr/ud5ZxFtRNTsYwMdmJeeeislw2+M72otrHxxvybCP7JWPPlJ38uhfajpcyhOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260721.1.tgz", + "integrity": "sha512-hon0lW4ZQ4boAVgaw+0ZFTNS8v5MWPWvK0HZnt4tDpKYnDUviLZawtUW3KqvFmCQTipVHl1S34j3J8Eqb93hGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260721.1.tgz", + "integrity": "sha512-nAl+HRQqpX5b7xVwWcvLPZmCk8NQ2yjI0yvJTWcHiRswbMEg1ZZckVmjJUAn0PHzZARbCSyIV7v3UjM+SPRmIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260721.1.tgz", + "integrity": "sha512-9paFG5cMTKz/CRixnEEnZbe5uvFPBFSDthxJHANfCWhUtBj49GSL1FPIokIg+Q+H8DGJEExU0lL92LtxD0lTxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "4.20260721.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260721.0.tgz", + "integrity": "sha512-fBLaCxZ2i/nPH8iyLzvza0C8/sSF4sjD1ma1Skf+pkZVK0TlaW5ujHJlUHwcwR66v2JZt+Q28d4DCX/oaLG0cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260721.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/wrangler/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260721.1.tgz", + "integrity": "sha512-b/DWhpV0jTudzQpLhDovcOgBz233386q+3Hbari7CLCNT9UXxjQziSTZ9yCoKdT2K3TSx5jrwlOisq8hlLWXYg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260721.1", + "@cloudflare/workerd-darwin-arm64": "1.20260721.1", + "@cloudflare/workerd-linux-64": "1.20260721.1", + "@cloudflare/workerd-linux-arm64": "1.20260721.1", + "@cloudflare/workerd-windows-64": "1.20260721.1" + } + }, + "node_modules/wrangler/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/ws": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", diff --git a/edge/package.json b/edge/package.json index 56d8a4f15..1b7f466c4 100644 --- a/edge/package.json +++ b/edge/package.json @@ -25,6 +25,6 @@ "loro-websocket": "^0.6.2", "typescript": "^6.0.3", "vitest": "^4.1.10", - "wrangler": "^4.111.0" + "wrangler": "4.113.0" } } diff --git a/edge/src/chat-room.ts b/edge/src/chat-room.ts index b5b64fa83..815150914 100644 --- a/edge/src/chat-room.ts +++ b/edge/src/chat-room.ts @@ -31,7 +31,9 @@ import { setMeta } from "./chat-log"; import { decodeFrame, encodeFrame, FRAME } from "./chat-frames"; -import { AUTH_USER_HEADER, type Env } from "./env"; +import { AUTH_ORG_HEADER, AUTH_USER_HEADER, ENCRYPTED_ROOM_HEADER, type Env } from "./env"; +import { profileRequiresEncryption } from "./vault-gate"; +import { looksLikeSealedContent } from "./vault-records"; const DAY_MS = 24 * 60 * 60 * 1000; /** Inbound frame budget: one pushed row (+ header slack). */ @@ -51,9 +53,12 @@ const QUOTA_MAX_BYTES = 8 * 1024 * 1024; interface SocketState { userId: string; + orgId?: string; device: string; /** Set once a valid hello established the session. */ ready?: boolean; + /** Encrypted room generation: pushed rows must be signed content records. */ + encrypted?: boolean; } interface PushOutcome { @@ -96,6 +101,12 @@ export class ChatRoom implements DurableObject { const sql = this.ctx.storage.sql; const owner = getMeta(sql, "owner"); + // Worker-stamped (never client-derived): this room is an encrypted + // generation, so every content body must be ciphertext-shaped — a + // client that regressed to plaintext is refused, not stored (RFC §12.2). + const encrypted = request.headers.get(ENCRYPTED_ROOM_HEADER) === "1"; + if (encrypted && getMeta(sql, "encrypted") !== "1") setMeta(sql, "encrypted", "1"); + const sealedOnly = encrypted || getMeta(sql, "encrypted") === "1"; if (url.pathname === "/ws") { // Claim-on-first-join ownership, then owner-only forever (the s2 @@ -106,7 +117,7 @@ export class ChatRoom implements DurableObject { const device = url.searchParams.get("device") ?? ""; const pair = new WebSocketPair(); this.ctx.acceptWebSocket(pair[1]); - const state: SocketState = { userId, device }; + const state: SocketState = { userId, device, encrypted: sealedOnly, orgId: request.headers.get(AUTH_ORG_HEADER) ?? undefined }; pair[1].serializeAttachment(state); return new Response(null, { status: 101, webSocket: pair[0] }); } @@ -130,9 +141,27 @@ export class ChatRoom implements DurableObject { } const body = new Uint8Array(await request.arrayBuffer()); if (body.byteLength > MAX_CHECKPOINT_BYTES) return json({ error: "too_large" }, 413); + if ( + sealedOnly && + (!looksLikeSealedContent(body, MAX_CHECKPOINT_BYTES, 2n) || + !looksLikeSealedContent(frontier, 65536, 3n)) + ) { + return json({ error: "plaintext_rejected" }, 400); + } + const expectedFrontier = request.headers.get("x-chat2-expected-frontier"); + if (expectedFrontier !== null && expectedFrontier !== encodeBase64(this.blobs.get(FRONTIER_BLOB) ?? new Uint8Array(0))) { + return json({ error: "checkpoint_changed" }, 409); + } const outcome = commitCheckpoint(sql, this.blobs, seqCovered, frontier, body, Date.now()); if (!outcome.ok) return json({ error: outcome.error }, 409); this.markBackupDirty(); + // A history import can add state without appending a row. Reconnect + // existing readers so their normal catch-up loads the new checkpoint. + if (sealedOnly && url.searchParams.get("refreshReaders") === "1") { + for (const socket of this.ctx.getWebSockets()) { + try { socket.close(4411, "encrypted history updated"); } catch { /* already closed */ } + } + } return json({ ok: true, seqFloor: outcome.seqFloor, pruned: outcome.pruned }); } @@ -255,6 +284,10 @@ export class ChatRoom implements DurableObject { return json({ error: "too_large" }, 413); } const payload = new Uint8Array(await request.arrayBuffer()); + if (sealedOnly && !looksLikeSealedContent(payload, MAX_ROW_BYTES, 1n)) { + this.recordPush(device, false); + return json({ error: "plaintext_rejected" }, 400); + } if (!this.admitQuota(device, payload.byteLength)) { this.recordPush(device, false); return json({ error: "quota" }, 429); @@ -282,6 +315,9 @@ export class ChatRoom implements DurableObject { const name = url.pathname === "/tail" ? "sidecar-tail" : "sidecar-diff"; const body = new Uint8Array(await request.arrayBuffer()); if (body.byteLength > MAX_SIDECAR_BYTES) return json({ error: "too_large" }, 413); + if (sealedOnly && !looksLikeSealedContent(body, MAX_SIDECAR_BYTES, url.pathname === "/tail" ? 5n : 6n)) { + return json({ error: "plaintext_rejected" }, 400); + } this.blobs.put(name, body); setMeta(sql, `${name}-type`, request.headers.get("content-type") ?? "application/json"); return json({ ok: true, bytes: body.byteLength }); @@ -360,6 +396,11 @@ export class ChatRoom implements DurableObject { this.handleRowsReq(ws, state, frame.header); return; case FRAME.push: + if (!state.encrypted && await profileRequiresEncryption(this.env, state.orgId, state.userId)) { + send(ws, FRAME.error, { code: "encrypted_profile", message: "plaintext writes refused", batchId: frame.header.batchId }); + ws.close(4403, "encrypted profile"); + return; + } this.handlePush(ws, state, frame.header, frame.payload); return; case FRAME.presence: @@ -437,6 +478,12 @@ export class ChatRoom implements DurableObject { send(ws, FRAME.error, { code: "bad_push", message: "hello first / malformed push", batchId }); return; } + if (state.encrypted && !looksLikeSealedContent(payload, MAX_ROW_BYTES, 1n)) { + // Permanent for this batch: the client must not replay plaintext. + this.recordPush(state.device, false); + send(ws, FRAME.error, { code: "plaintext_rejected", message: "encrypted room: signed content records only", batchId }); + return; + } if (!this.admitQuota(state.device, payload.byteLength)) { this.recordPush(state.device, false); send(ws, FRAME.error, { code: "quota", message: "per-device push quota exceeded", batchId }); diff --git a/edge/src/device-room.ts b/edge/src/device-room.ts index 5e6cac0ec..e1656ed84 100644 --- a/edge/src/device-room.ts +++ b/edge/src/device-room.ts @@ -16,7 +16,8 @@ */ import { BytesReader, BytesWriter } from "loro-protocol"; import { createBlobStore, getJsonBlob, putJsonBlob, type BlobStore } from "./blobs"; -import { AUTH_USER_HEADER, type Env } from "./env"; +import { AUTH_ORG_HEADER, AUTH_USER_HEADER, type Env } from "./env"; +import { profileRequiresEncryption } from "./vault-gate"; export interface DeviceFrameHeader { /** Stream id, unique per (connId, logical stream). */ @@ -47,6 +48,7 @@ export const decodeDeviceFrame = ( interface SocketState { userId: string; + orgId?: string; role: "host" | "client"; connId: string; /** Accept time — the liveness floor until the socket's first auto-pong. */ @@ -74,6 +76,14 @@ const clientTag = (connId: string) => `client:${connId}`; * — 2.5 of their intervals — so upgrading engines is never a prerequisite. */ const HOST_LIVENESS_MS = 75_000; +/** The authenticated device channel (RFC 0001 §10, crates/rpc/src/device_channel.rs): + * Noise handshake and sealed RPC frames. For a profile that requires + * encryption these are the only application frames the relay carries — the + * relay sees ciphertext and routing, nothing else. */ +export const CHANNEL_KIND = "chan"; +/** App-level liveness echo (empty payload, bounced by the host). */ +const ECHO_KIND = "echo"; + /** Control frames the relay itself emits (kind " relay"). */ // MUST byte-match packages/rpc device-frames.ts RELAY_KIND — clients compare // with ===; a mismatch makes host_offline/host_closed invisible to them. @@ -90,10 +100,11 @@ const CHAT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/; export class DeviceRoom implements DurableObject { private readonly ctx: DurableObjectState; private readonly blobs: BlobStore; + private readonly env: Env; constructor(ctx: DurableObjectState, env: Env) { this.ctx = ctx; - void env; + this.env = env; ctx.storage.sql.exec( "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)" ); @@ -173,7 +184,7 @@ export class DeviceRoom implements DurableObject { } else { this.ctx.acceptWebSocket(pair[1], [clientTag(connId)]); } - const state: SocketState = { userId, role, connId, joinedAt: Date.now() }; + const state: SocketState = { userId, role, connId, joinedAt: Date.now(), orgId: request.headers.get(AUTH_ORG_HEADER) ?? undefined }; pair[1].serializeAttachment(state); return new Response(null, { status: 101, webSocket: pair[0] }); } @@ -188,6 +199,9 @@ export class DeviceRoom implements DurableObject { return value === undefined ? json({ error: "not_found" }, 404) : json(value); } if (request.method === "POST") { + if (await profileRequiresEncryption(this.env, request.headers.get(AUTH_ORG_HEADER) ?? undefined, userId)) { + return json({ error: "encrypted_channel_required" }, 409); + } putJsonBlob(this.blobs, `sidecar:${name}`, await request.json()); return json({ ok: true }); } @@ -250,7 +264,7 @@ export class DeviceRoom implements DurableObject { this.ctx.storage.sql.exec("DELETE FROM pending_nudges"); } - webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): void { + async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise { if (typeof message === "string") return; // ping/pong auto-response const state = ws.deserializeAttachment() as SocketState; let frame: { header: DeviceFrameHeader; payload: Uint8Array }; @@ -260,6 +274,14 @@ export class DeviceRoom implements DurableObject { ws.close(1002, "Frame error"); return; } + // Encrypted profile: only channel frames (handshake + ciphertext) and + // payload-less liveness echoes cross the relay. A plaintext RPC frame is + // a protocol violation — the socket closes; nothing is forwarded. + const opaque = frame.header.k === CHANNEL_KIND || (frame.header.k === ECHO_KIND && frame.payload.length === 0); + if (!opaque && (await profileRequiresEncryption(this.env, state.orgId, state.userId))) { + ws.close(4403, "encrypted device channel required"); + return; + } if (state.role === "client") { const host = this.liveHost(); if (!host) { diff --git a/edge/src/env.ts b/edge/src/env.ts index fbd657042..4830b5d50 100644 --- a/edge/src/env.ts +++ b/edge/src/env.ts @@ -7,6 +7,9 @@ export interface Env { /** chat2 session rooms (`chat2/{chatId}`) — dumb authenticated log relays * replacing SessionRoom's loro-aware s2 rooms (docs/chat2-sync.md). */ CHAT_ROOMS: DurableObjectNamespace; + /** Per-profile encrypted-sync control plane (`vault1/{orgId}/{userId}`): + * signed membership history, key envelopes, enrollment (RFC 0001 §6). */ + VAULT_ROOMS: DurableObjectNamespace; BLOBS: R2Bucket; /** Release artifacts (headless tarballs, dmgs, latest.txt) served at * /releases/* for the curl-install flow. */ @@ -27,9 +30,16 @@ export interface Env { * the caller's JWT. DOs trust it blindly — they are only reachable through * the Worker (design §2: "DO never sees an unauthenticated frame"). */ export const AUTH_USER_HEADER = "x-zeron-auth-user"; +export const AUTH_ORG_HEADER = "x-zeron-auth-org"; /** Header the Worker stamps on requests forwarded into workspace-doc rooms * (`ws/{orgId}`). Membership (JWT org claim == orgId) is enforced at the * Worker; the SessionRoom DO sees this and skips its per-chat * claim-on-first-join ownership discipline for the room. */ export const ROOM_KIND_HEADER = "x-zeron-room-kind"; + +/** Header the Worker stamps on requests forwarded into an ENCRYPTED room + * generation (`chat2/{id}-e1`, `reg1e1/…`). The DO then enforces protocol + * framing: every stored content byte must parse as a signed content record + * (RFC 0001 §12.2). Never derived from client input. */ +export const ENCRYPTED_ROOM_HEADER = "x-zeron-room-encrypted"; diff --git a/edge/src/index.ts b/edge/src/index.ts index 174a109e7..a335a493e 100644 --- a/edge/src/index.ts +++ b/edge/src/index.ts @@ -35,17 +35,21 @@ * GET|PUT /chat2/:chatId/diff * GET /chat2/:chatId/stats * POST /chat2/:chatId/reset + * ANY /vault/:orgId/* — encrypted-sync control plane `vault1/{orgId}/{user}` */ import { authenticate } from "./auth"; import { handleAuthRoute } from "./auth-routes"; -import { AUTH_USER_HEADER, ROOM_KIND_HEADER, type Env } from "./env"; +import { AUTH_ORG_HEADER, AUTH_USER_HEADER, ENCRYPTED_ROOM_HEADER, ROOM_KIND_HEADER, type Env } from "./env"; +import { looksLikeSealedContent } from "./vault-records"; +import { profileRequiresEncryption } from "./vault-gate"; import { SessionRoom } from "./session-room"; import { DeviceRoom } from "./device-room"; import { RegistryRoom } from "./registry-room"; import { ChatRoom } from "./chat-room"; +import { VaultRoom } from "./vault-room"; import installSh from "./install.sh"; -export { SessionRoom, DeviceRoom, RegistryRoom, ChatRoom }; +export { SessionRoom, DeviceRoom, RegistryRoom, ChatRoom, VaultRoom }; const ID_RE = /^[A-Za-z0-9_-]{1,128}$/; @@ -76,7 +80,8 @@ const forward = ( userId: string, path: string, search?: string, - roomKind?: "workspace" + roomKind?: "workspace", + encrypted = false ): Promise => { const stub = ns.get(ns.idFromName(name)); const url = new URL(request.url); @@ -87,13 +92,53 @@ const forward = ( // workspace rooms): clear any inbound value so only the explicit set below — // reached solely on workspace forwards, after the org-membership check — // can assert it. Do not drop this line; passthrough would let a caller - // choose their own room kind. + // choose their own room kind. Same for the encrypted-room stamp. headers.delete(ROOM_KIND_HEADER); + headers.delete(ENCRYPTED_ROOM_HEADER); headers.set(AUTH_USER_HEADER, userId); if (roomKind) headers.set(ROOM_KIND_HEADER, roomKind); + if (encrypted) headers.set(ENCRYPTED_ROOM_HEADER, "1"); return stub.fetch(new Request(url.toString(), { ...requestInit(request), headers })); }; +/** Encrypted chat rooms carry the `-e1` generation suffix (engine + * `encrypted_room_id`); the hashed long-id form starts with `e1-`. */ +const isEncryptedChatRoom = (chatId: string): boolean => + chatId.endsWith("-e1") || chatId.startsWith("e1-"); + +/** Per-isolate memo of "this profile has an encrypted vault" — the legacy + * write fence (RFC 0001 §12.2). A vault exists ⇒ the profile's plaintext + * rooms are retained read-only and refuse new writes; the encrypted + * generations are unaffected. Absence of an org claim (dev bearers without + * `@org`) cannot activate a fence. */ +const VAULT_ACTIVE_TTL_MS = 30_000; +const vaultActiveMemo = new Map(); +const vaultActive = async (env: Env, orgId: string | undefined, userId: string): Promise => { + if (!orgId) return profileRequiresEncryption(env, orgId, userId); + const key = `${orgId}/${userId}`; + const memo = vaultActiveMemo.get(key); + const now = Date.now(); + if (memo?.active && now - memo.at < VAULT_ACTIVE_TTL_MS) return true; + // Unknown ⇒ refuse writes; absence is never cached across activation. + const active = await profileRequiresEncryption(env, orgId, userId); + if (active) { + if (vaultActiveMemo.size >= 1024) vaultActiveMemo.clear(); + vaultActiveMemo.set(key, { active, at: now }); + } else { + vaultActiveMemo.delete(key); + } + return active; +}; + +const encryptedProfileRefusal = (): Response => + json( + { + error: "encrypted_profile", + message: "this account uses end-to-end encryption; plaintext writes are refused (upgrade this client)" + }, + 409 + ); + const requestInit = (request: Request): RequestInit => ({ method: request.method, body: request.body @@ -158,9 +203,14 @@ export default { const auth = await authenticate(env, request); if (!auth) return json({ error: "unauthenticated" }, 401); + const scopedHeaders = new Headers(request.headers); + scopedHeaders.delete(AUTH_ORG_HEADER); + if (auth.orgId) scopedHeaders.set(AUTH_ORG_HEADER, auth.orgId); + request = new Request(request, { headers: scopedHeaders }); // ── session rooms ─────────────────────────────────────────────────────── if (parts[0] === "session" && parts[1] && ID_RE.test(parts[1]) && parts[2] === "ws") { + if (await vaultActive(env, auth.orgId, auth.userId)) return encryptedProfileRefusal(); if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") { return json({ error: "expected websocket" }, 426); } @@ -185,12 +235,16 @@ export default { return forward(env.SESSION_ROOMS, `s2/${parts[1]}`, request, auth.userId, "/stats", ""); } if (parts[0] === "diff" && parts[1] && ID_RE.test(parts[1])) { + if (request.method === "POST" && (await vaultActive(env, auth.orgId, auth.userId))) { + return encryptedProfileRefusal(); + } return forward(env.SESSION_ROOMS, `s2/${parts[1]}`, request, auth.userId, "/diff", ""); } if (parts[0] === "snapshot" && parts[1] && ID_RE.test(parts[1]) && request.method === "GET") { return forward(env.SESSION_ROOMS, `s2/${parts[1]}`, request, auth.userId, "/snapshot", ""); } if (parts[0] === "append" && parts[1] && ID_RE.test(parts[1]) && request.method === "POST") { + if (await vaultActive(env, auth.orgId, auth.userId)) return encryptedProfileRefusal(); return forward(env.SESSION_ROOMS, `s2/${parts[1]}`, request, auth.userId, "/append", ""); } @@ -201,6 +255,20 @@ export default { // /stats, /reset. ────────────────────────────────────────────────────── if (parts[0] === "chat2" && parts[1] && ID_RE.test(parts[1]) && parts[2]) { const room = `chat2/${parts[1]}`; + const encrypted = isEncryptedChatRoom(parts[1]); + // Legacy-write fence: once the profile has a vault, its plaintext + // chat rooms accept no new sockets or writes (reads stay for the + // retained copy / migration). Encrypted rooms are unaffected. + const legacyWrite = + !encrypted && + (parts[2] === "ws" || + (parts[2] === "rows" && request.method === "POST") || + (parts[2] === "checkpoint" && request.method === "POST") || + ((parts[2] === "tail" || parts[2] === "diff") && request.method === "PUT") || + parts[2] === "reset"); + if (legacyWrite && (await vaultActive(env, auth.orgId, auth.userId))) { + return encryptedProfileRefusal(); + } if (parts[2] === "ws" && parts.length === 3) { if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") { return json({ error: "expected websocket" }, 426); @@ -211,7 +279,9 @@ export default { request, auth.userId, "/ws", - `?chatId=${parts[1]}${deviceParam(url)}` + `?chatId=${parts[1]}${deviceParam(url)}`, + undefined, + encrypted ); } const routes: Record = { @@ -228,7 +298,16 @@ export default { if (parts.length === 3 && routes[parts[2]]?.includes(request.method)) { // Query carries through (`seqCovered` on POST /checkpoint), as do // headers (`x-chat2-frontier`, `range`). - return forward(env.CHAT_ROOMS, room, request, auth.userId, `/${parts[2]}`, url.search); + return forward( + env.CHAT_ROOMS, + room, + request, + auth.userId, + `/${parts[2]}`, + url.search, + undefined, + encrypted + ); } return json({ error: "not found" }, 404); } @@ -249,6 +328,9 @@ export default { // (hibernated, ~zero cost). URL path stays `/workspace/:orgId/*`; the // name is worker-internal — clients echo their own roomId strings. const room = `ws4/${orgId}/${auth.userId}`; + if ((parts[2] === "ws" || request.method === "POST") && await vaultActive(env, orgId, auth.userId)) { + return encryptedProfileRefusal(); + } if (parts[2] === "ws") { if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") { return json({ error: "expected websocket" }, 426); @@ -302,8 +384,21 @@ export default { if (parts[0] === "registry" && parts[1] && ID_RE.test(parts[1])) { const orgId = parts[1]; if (auth.orgId !== orgId) return json({ error: "forbidden" }, 403); - const room = `reg1/${orgId}/${auth.userId}`; - if (parts[2] === "ws") { + // Encrypted profiles address a SEPARATE registry generation + // (`/registry/:orgId/e1/*` → `reg1e1/{org}/{user}`, RFC 0001 §9, §12): + // sealed field values never share a table with the legacy plaintext + // rows, and the plaintext room is retained for the cleanup step. + const encrypted = parts[2] === "e1"; + const room = encrypted ? `reg1e1/${orgId}/${auth.userId}` : `reg1/${orgId}/${auth.userId}`; + const leaf = encrypted ? parts[3] : parts[2]; + // Legacy-write fence (see chat2): the plaintext registry accepts no + // sockets or pushes once the profile has a vault; `rows` reads stay. + const legacyWrite = + !encrypted && (leaf === "ws" || leaf === "push" || leaf === "reset"); + if (legacyWrite && (await vaultActive(env, auth.orgId, auth.userId))) { + return encryptedProfileRefusal(); + } + if (leaf === "ws") { if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") { return json({ error: "expected websocket" }, 426); } @@ -313,32 +408,59 @@ export default { request, auth.userId, "/ws", - `?${deviceParam(url).replace(/^&/, "")}` + `?${deviceParam(url).replace(/^&/, "")}`, + undefined, + encrypted ); } - if (parts[2] === "stats" && request.method === "GET") { + if (leaf === "stats" && request.method === "GET") { return forward(env.REGISTRY_ROOMS, room, request, auth.userId, "/stats", ""); } // Pull over plain HTTPS: `?since=` returns the same delta the WS // hello would (full table without it — the original repair read). // One round trip on any network that passes HTTPS at all, where the // WS upgrade needs 4 and a cooperative middlebox. - if (parts[2] === "rows" && request.method === "GET") { + if (leaf === "rows" && request.method === "GET") { return forward(env.REGISTRY_ROOMS, room, request, auth.userId, "/rows", url.search); } // Push over plain HTTPS — the WS push's fallback twin (LWW clocks // make replays no-ops, so at-least-once delivery is safe). - if (parts[2] === "push" && request.method === "POST") { - return forward(env.REGISTRY_ROOMS, room, request, auth.userId, "/push", url.search); + if (leaf === "push" && request.method === "POST") { + return forward( + env.REGISTRY_ROOMS, + room, + request, + auth.userId, + "/push", + url.search, + undefined, + encrypted + ); } // Operator wipe. Unlike the CRDT rooms this needs no recipe: clients // detect the seq regression on their next hello and re-seed the table // from local rows with original clocks, automatically. - if (parts[2] === "reset" && request.method === "POST") { + if (leaf === "reset" && request.method === "POST") { return forward(env.REGISTRY_ROOMS, room, request, auth.userId, "/reset", ""); } } + // ── vault control plane (RFC 0001 §6): per-(org, user) like the + // registry — org claim must match, room derived from the caller's OWN + // user id. The DO validates every sub-path and authenticates every + // write cryptographically; the bearer only proves account identity. + // `vault1` = first control-plane generation. ──────────────────────── + if (parts[0] === "vault" && parts[1] && ID_RE.test(parts[1])) { + const orgId = parts[1]; + if (auth.orgId !== orgId) return json({ error: "forbidden" }, 403); + const room = `vault1/${orgId}/${auth.userId}`; + const sub = parts.slice(2); + if (sub.length > 3 || !sub.every((segment) => /^[A-Za-z0-9]{1,64}$/.test(segment))) { + return json({ error: "not found" }, 404); + } + return forward(env.VAULT_ROOMS, room, request, auth.userId, `/${sub.join("/")}`, url.search); + } + // ── device rooms ──────────────────────────────────────────────────────── if (parts[0] === "device" && parts[1] && ID_RE.test(parts[1])) { const deviceId = parts[1]; @@ -392,6 +514,13 @@ export default { // Outputs are 4KiB-capped at the harness boundary; diffs can run // larger but a sidecar entry is one tool result, never a dump. if (body.byteLength > MAX_TOOL_BLOB_BYTES) return json({ error: "too_large" }, 413); + // Encrypted profile: only ciphertext-shaped blobs are stored. + if ( + (await vaultActive(env, auth.orgId, auth.userId)) && + !looksLikeSealedContent(new Uint8Array(body), MAX_TOOL_BLOB_BYTES, 7n) + ) { + return encryptedProfileRefusal(); + } await env.BLOBS.put(key, body, { httpMetadata: { contentType: request.headers.get("content-type") ?? "text/plain; charset=utf-8" diff --git a/edge/src/modules.d.ts b/edge/src/modules.d.ts index c675add3b..e848f2561 100644 --- a/edge/src/modules.d.ts +++ b/edge/src/modules.d.ts @@ -4,3 +4,11 @@ declare module "*.sh" { const text: string; export default text; } + +// Node-only test helpers (vitest unit tier reads the shared Rust fixture). +declare module "node:fs" { + export function readFileSync(path: string | URL, encoding: "utf8"): string; +} +interface ImportMeta { + url: string; +} diff --git a/edge/src/registry-core.test.ts b/edge/src/registry-core.test.ts index eeb5bb6d5..762fe15a5 100644 --- a/edge/src/registry-core.test.ts +++ b/edge/src/registry-core.test.ts @@ -21,6 +21,14 @@ const upsert = (over: Partial = {}): Op => ({ ...over }); +const del = (at: string, proof?: Op["proof"]): Op => ({ + kind: "chats", + id: "chat-1", + op: "delete", + hlc: at, + ...(proof !== undefined ? { proof } : {}) +}); + const applied = (row: Row | undefined, op: Op): Row => { const result = applyOp(row, op); expect(result.changed).toBe(true); @@ -172,6 +180,24 @@ describe("applyOp", () => { expect(stale.changed).toBe(false); }); + it("carries the lifecycle proof on the tombstone and clears it on revival", () => { + const proof = { e1: "c2VhbGVk" }; + const gone = applied(applied(undefined, upsert()), del(hlc(2000), proof)); + expect(gone.deleted).toBe(true); + expect(gone.delProof).toEqual(proof); + // A newer delete replaces the proof; an older one leaves it untouched. + const again = applied(gone, del(hlc(3000), { e1: "bmV3ZXI=" })); + expect(again.delProof).toEqual({ e1: "bmV3ZXI=" }); + expect(applyOp(again, del(hlc(2500), proof)).changed).toBe(false); + // Re-seeding a tombstone carries its proof back as the op's `proof`. + expect(rowToSeedOp(again)).toEqual({ kind: "chats", id: "chat-1", op: "delete", hlc: hlc(3000), proof: { e1: "bmV3ZXI=" } }); + // Revival drops the proof with the tombstone it authorized. + const revived = applied(again, upsert({ hlc: hlc(4000) })); + expect(revived.deleted).toBe(false); + expect(revived.delProof).toBeUndefined(); + expect(revived.delHlc).toBe(hlc(3000)); + }); + it("rowToSeedOp round-trips tombstones", () => { const gone = applied(undefined, { kind: "spaces", id: "sp-1", op: "delete", hlc: hlc(7000) }); const seeded = applied(undefined, rowToSeedOp(gone)); @@ -230,6 +256,13 @@ describe("applyOp", () => { }); describe("validateOp", () => { + it("allows a proof only on delete ops, and only as an object", () => { + expect(validateOp({ ...upsert(), proof: { e1: "x" } })).toBe("bad proof"); + expect(validateOp(del(hlc(1), "nope" as unknown as Op["proof"]))).toBe("bad proof"); + expect(validateOp(del(hlc(1), { e1: "x" }))).toBeNull(); + expect(validateOp(del(hlc(1)))).toBeNull(); + }); + it("accepts well-formed ops and rejects malformed ones", () => { expect(validateOp(upsert())).toBeNull(); expect(validateOp({ ...upsert(), kind: "Nope Kind" })).toMatch(/kind/); diff --git a/edge/src/registry-core.ts b/edge/src/registry-core.ts index 60381e32c..aa93179ea 100644 --- a/edge/src/registry-core.ts +++ b/edge/src/registry-core.ts @@ -31,6 +31,11 @@ export interface Row { deleted: boolean; /** Tombstone clock — an upsert newer than this revives the row. */ delHlc?: Hlc; + /** Lifecycle proof for the tombstone (RFC 0001 §9): the sealed record a + * member authored for exactly this row and `delHlc`. Readers of an + * encrypted generation accept a tombstone only with a verified proof; + * cleared on revival. Opaque to the relay. */ + delProof?: FieldValue; fields: Record; /** Per-field last-write clocks. */ clocks: Record; @@ -50,6 +55,9 @@ export interface Op { /** Per-field clock overrides — used by re-seed pushes to carry a row's * ORIGINAL clocks so recovery never coarsens causality. */ clocks?: Record; + /** Lifecycle proof for a `delete` op (stored as the tombstone's + * `delProof`). Only meaningful on deletes; required in encrypted rooms. */ + proof?: FieldValue; } const ID_RE = /^[A-Za-z0-9_.:@\/-]{1,256}$/; @@ -66,6 +74,9 @@ export const validateOp = (op: Op): string | null => { if (!ID_RE.test(op.id)) return "bad id"; if (op.op !== "upsert" && op.op !== "update" && op.op !== "delete") return "bad op"; if (!HLC_RE.test(op.hlc)) return "bad hlc"; + if (op.proof !== undefined && (op.op !== "delete" || typeof op.proof !== "object" || op.proof === null)) { + return "bad proof"; + } if (op.op === "delete") { if (op.set !== undefined) return "delete carries set"; } else { @@ -110,13 +121,24 @@ export const applyOp = (row: Row | undefined, op: Op): ApplyResult => { if (row === undefined) { // Tombstone-on-missing guards against a late create racing the delete. return { - row: { kind: op.kind, id: op.id, seq: 0, deleted: true, delHlc: op.hlc, fields: {}, clocks: {} }, + row: { + kind: op.kind, id: op.id, seq: 0, deleted: true, delHlc: op.hlc, + ...(op.proof !== undefined ? { delProof: op.proof } : {}), + fields: {}, clocks: {} + }, changed: true }; } if (row.deleted ? hlcNewer(op.hlc, row.delHlc) : hlcNewer(op.hlc, maxClock(row))) { + // The proof travels with the tombstone it authorizes; an older proof + // never lingers on a newer tombstone. + const { delProof: _stale, ...rest } = row; return { - row: { ...row, deleted: true, delHlc: op.hlc, fields: {}, clocks: {} }, + row: { + ...rest, deleted: true, delHlc: op.hlc, + ...(op.proof !== undefined ? { delProof: op.proof } : {}), + fields: {}, clocks: {} + }, changed: true }; } @@ -169,7 +191,10 @@ export const maxClock = (row: Row): Hlc | undefined => { * the row's original per-field clocks, or a delete for tombstones. */ export const rowToSeedOp = (row: Row): Op => { if (row.deleted) { - return { kind: row.kind, id: row.id, op: "delete", hlc: row.delHlc ?? encodeHlc(0, 0, "seed") }; + return { + kind: row.kind, id: row.id, op: "delete", hlc: row.delHlc ?? encodeHlc(0, 0, "seed"), + ...(row.delProof !== undefined ? { proof: row.delProof } : {}) + }; } const max = maxClock(row) ?? encodeHlc(0, 0, "seed"); return { diff --git a/edge/src/registry-room.ts b/edge/src/registry-room.ts index 6f0bf3ce9..f49f91143 100644 --- a/edge/src/registry-room.ts +++ b/edge/src/registry-room.ts @@ -19,8 +19,10 @@ * Hibernation discipline: ZERO wall-clock timers; ping/pong rides the * auto-response pair; the daily alarm does tombstone GC + the R2 backup. */ -import { applyOp, validateOp, type Op, type Row } from "./registry-core"; -import { AUTH_USER_HEADER, type Env } from "./env"; +import { applyOp, validateOp, type FieldValue, type Op, type Row } from "./registry-core"; +import { AUTH_ORG_HEADER, AUTH_USER_HEADER, ENCRYPTED_ROOM_HEADER, type Env } from "./env"; +import { profileRequiresEncryption } from "./vault-gate"; +import { looksLikeSealedField, looksLikeSealedLifecycle } from "./vault-records"; const DAY_MS = 24 * 60 * 60 * 1000; /** Tombstones older than this are purged; cursors from before the purge @@ -34,6 +36,8 @@ const MAX_FRAME_BYTES = 1_000_000; interface SocketState { userId: string; + orgId?: string; + encrypted?: boolean; device: string; /** Set once a valid hello established the session. */ ready?: boolean; @@ -60,6 +64,13 @@ export class RegistryRoom implements DurableObject { "CREATE TABLE IF NOT EXISTS rows (kind TEXT NOT NULL, id TEXT NOT NULL, seq INTEGER NOT NULL, deleted INTEGER NOT NULL, del_hlc TEXT, fields TEXT NOT NULL, clocks TEXT NOT NULL, PRIMARY KEY (kind, id))" ); ctx.storage.sql.exec("CREATE INDEX IF NOT EXISTS rows_seq ON rows (seq)"); + // Lifecycle proofs (RFC 0001 §9) ride tombstones; rooms created before + // the column existed gain it here (SQLite has no ADD COLUMN IF NOT EXISTS). + try { + ctx.storage.sql.exec("ALTER TABLE rows ADD COLUMN del_proof TEXT"); + } catch { + /* column exists */ + } ctx.storage.sql.exec( "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)" ); @@ -97,32 +108,25 @@ export class RegistryRoom implements DurableObject { private loadRow(kind: string, id: string): Row | undefined { const rows = [ ...this.ctx.storage.sql.exec( - "SELECT seq, deleted, del_hlc, fields, clocks FROM rows WHERE kind = ? AND id = ?", + "SELECT seq, deleted, del_hlc, del_proof, fields, clocks FROM rows WHERE kind = ? AND id = ?", kind, id ) ]; const raw = rows[0]; if (!raw) return undefined; - return { - kind, - id, - seq: raw.seq as number, - deleted: (raw.deleted as number) === 1, - ...(raw.del_hlc ? { delHlc: raw.del_hlc as string } : {}), - fields: JSON.parse(raw.fields as string) as Row["fields"], - clocks: JSON.parse(raw.clocks as string) as Row["clocks"] - }; + return rowFromRaw(kind, id, raw); } private saveRow(row: Row): void { this.ctx.storage.sql.exec( - "INSERT INTO rows (kind, id, seq, deleted, del_hlc, fields, clocks) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(kind, id) DO UPDATE SET seq = excluded.seq, deleted = excluded.deleted, del_hlc = excluded.del_hlc, fields = excluded.fields, clocks = excluded.clocks", + "INSERT INTO rows (kind, id, seq, deleted, del_hlc, del_proof, fields, clocks) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(kind, id) DO UPDATE SET seq = excluded.seq, deleted = excluded.deleted, del_hlc = excluded.del_hlc, del_proof = excluded.del_proof, fields = excluded.fields, clocks = excluded.clocks", row.kind, row.id, row.seq, row.deleted ? 1 : 0, row.delHlc ?? null, + row.delProof === undefined ? null : JSON.stringify(row.delProof), JSON.stringify(row.fields), JSON.stringify(row.clocks) ); @@ -131,18 +135,10 @@ export class RegistryRoom implements DurableObject { private rowsSince(cursor: number): Row[] { const out: Row[] = []; for (const raw of this.ctx.storage.sql.exec( - "SELECT kind, id, seq, deleted, del_hlc, fields, clocks FROM rows WHERE seq > ? ORDER BY seq", + "SELECT kind, id, seq, deleted, del_hlc, del_proof, fields, clocks FROM rows WHERE seq > ? ORDER BY seq", cursor )) { - out.push({ - kind: raw.kind as string, - id: raw.id as string, - seq: raw.seq as number, - deleted: (raw.deleted as number) === 1, - ...(raw.del_hlc ? { delHlc: raw.del_hlc as string } : {}), - fields: JSON.parse(raw.fields as string) as Row["fields"], - clocks: JSON.parse(raw.clocks as string) as Row["clocks"] - }); + out.push(rowFromRaw(raw.kind as string, raw.id as string, raw)); } return out; } @@ -154,12 +150,14 @@ export class RegistryRoom implements DurableObject { const url = new URL(request.url); const userId = request.headers.get(AUTH_USER_HEADER); if (!userId) return json({ error: "unauthenticated" }, 401); + if (request.headers.get(ENCRYPTED_ROOM_HEADER) === "1") this.setMeta("encrypted", "1"); + const encrypted = this.getMeta("encrypted") === "1"; if (url.pathname === "/ws") { const device = url.searchParams.get("device") ?? ""; const pair = new WebSocketPair(); this.ctx.acceptWebSocket(pair[1]); - const state: SocketState = { userId, device }; + const state: SocketState = { userId, device, encrypted, orgId: request.headers.get(AUTH_ORG_HEADER) ?? undefined }; pair[1].serializeAttachment(state); return new Response(null, { status: 101, webSocket: pair[0] }); } @@ -232,7 +230,7 @@ export class RegistryRoom implements DurableObject { } catch { return json({ error: "bad_push", message: "malformed body" }, 400); } - const outcome = this.applyPushBatch(device, frame); + const outcome = this.applyPushBatch(device, frame, encrypted); if (!outcome.ok) return json({ error: outcome.code, message: outcome.message }, 400); return json({ batch: outcome.batch, seq: outcome.seq, applied: outcome.applied }); } @@ -281,6 +279,11 @@ export class RegistryRoom implements DurableObject { this.handleHello(ws, state, frame); return; case "push": + if (!state.encrypted && await profileRequiresEncryption(this.env, state.orgId, state.userId)) { + send(ws, { t: "error", code: "encrypted_profile", message: "plaintext writes refused" }); + ws.close(4403, "encrypted profile"); + return; + } this.handlePush(ws, state, frame); return; case "presence": @@ -333,7 +336,7 @@ export class RegistryRoom implements DurableObject { send(ws, { t: "error", code: "bad_push", message: "hello first / malformed push" }); return; } - const outcome = this.applyPushBatch(state.device, frame); + const outcome = this.applyPushBatch(state.device, frame, state.encrypted === true); if (!outcome.ok) { send(ws, { t: "error", code: outcome.code, message: outcome.message }); return; @@ -346,7 +349,8 @@ export class RegistryRoom implements DurableObject { * fallback. The caller delivers the ack/error on its own transport. */ private applyPushBatch( device: string, - frame: Record + frame: Record, + encrypted: boolean ): | { ok: false; code: string; message: string } | { ok: true; batch: string; seq: number; applied: number } { @@ -369,6 +373,16 @@ export class RegistryRoom implements DurableObject { this.recordPush(device, false); return { ok: false, code: "invalid_op", message: `${op.kind}/${op.id}: ${invalid}` }; } + if (encrypted && Object.values(op.set ?? {}).some((value) => !looksLikeSealedField(value, 16 * 1024))) { + this.recordPush(device, false); + return { ok: false, code: "plaintext_rejected", message: "encrypted field records required" }; + } + // An encrypted generation never tombstones on a bare delete: the row + // lifecycle proof (RFC 0001 §9) must be present and framed as such. + if (encrypted && op.op === "delete" && !looksLikeSealedLifecycle(op.proof, 1024)) { + this.recordPush(device, false); + return { ok: false, code: "plaintext_rejected", message: "encrypted lifecycle proof required" }; + } } // Apply atomically: DO events are single-threaded and SQLite writes in an @@ -490,3 +504,15 @@ const json = (value: unknown, status = 200): Response => status, headers: { "content-type": "application/json" } }); + +/** A stored row → wire row; `del_proof` is JSON text or NULL. */ +const rowFromRaw = (kind: string, id: string, raw: Record): Row => ({ + kind, + id, + seq: raw.seq as number, + deleted: (raw.deleted as number) === 1, + ...(raw.del_hlc ? { delHlc: raw.del_hlc as string } : {}), + ...(raw.del_proof ? { delProof: JSON.parse(raw.del_proof as string) as FieldValue } : {}), + fields: JSON.parse(raw.fields as string) as Row["fields"], + clocks: JSON.parse(raw.clocks as string) as Row["clocks"] +}); diff --git a/edge/src/session-room.ts b/edge/src/session-room.ts index c6ecc53a0..dd5753f6b 100644 --- a/edge/src/session-room.ts +++ b/edge/src/session-room.ts @@ -50,7 +50,8 @@ import { } from "./session-doc"; import { createBlobStore, getJsonBlob, putJsonBlob, type BlobStore } from "./blobs"; import { appendUpdateRow, ensureUpdateLog, readUpdateRows } from "./update-log"; -import { AUTH_USER_HEADER, ROOM_KIND_HEADER, type Env } from "./env"; +import { AUTH_ORG_HEADER, AUTH_USER_HEADER, ROOM_KIND_HEADER, type Env } from "./env"; +import { profileRequiresEncryption } from "./vault-gate"; const DAY_MS = 24 * 60 * 60 * 1000; const RETAIN_MS = RETAIN_DAYS * DAY_MS; @@ -124,6 +125,7 @@ const isWasmUseAfterFree = (e: unknown): boolean => interface SocketState { userId: string; + orgId?: string; /** Joined sub-rooms by crdt magic ("%LOR", "%EPH"). */ rooms: string[]; /** True for sockets on a workspace-doc room — org membership was enforced @@ -232,6 +234,7 @@ export class SessionRoom implements DurableObject { this.ctx.acceptWebSocket(pair[1]); const state: SocketState = { userId, + orgId: request.headers.get(AUTH_ORG_HEADER) ?? undefined, rooms: [], ...(workspace ? { workspace } : {}), ...(deviceId ? { deviceId } : {}) @@ -415,6 +418,10 @@ export class SessionRoom implements DurableObject { return; } const state = ws.deserializeAttachment() as SocketState; + if (await profileRequiresEncryption(this.env, state.orgId, state.userId)) { + ws.close(4403, "encrypted profile"); + return; + } try { switch (decoded.type) { case MessageType.JoinRequest: diff --git a/edge/src/vault-gate.test.ts b/edge/src/vault-gate.test.ts new file mode 100644 index 000000000..c2e0e7d02 --- /dev/null +++ b/edge/src/vault-gate.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { vaultIsActive } from "./vault-gate"; + +describe("legacy write fence", () => { + it.each([200, 400, 401, 403, 429, 500, 503])("refuses plaintext on descriptor status %s", async (status) => { + expect(await vaultIsActive(async () => new Response(null, { status }))).toBe(true); + }); + + it("only permits an explicit absent-vault response", async () => { + expect(await vaultIsActive(async () => new Response(null, { status: 404 }))).toBe(false); + }); + + it("fails closed on a control-plane exception", async () => { + expect(await vaultIsActive(async () => { throw new Error("unavailable"); })).toBe(true); + }); + + it("rechecks absence after activation", async () => { + let status = 404; + const lookup = async () => new Response(null, { status }); + expect(await vaultIsActive(lookup)).toBe(false); + status = 200; + expect(await vaultIsActive(lookup)).toBe(true); + }); +}); diff --git a/edge/src/vault-gate.ts b/edge/src/vault-gate.ts new file mode 100644 index 000000000..16330a9f0 --- /dev/null +++ b/edge/src/vault-gate.ts @@ -0,0 +1,21 @@ +import { AUTH_USER_HEADER, type Env } from "./env"; + +export async function vaultIsActive(lookup: () => Promise): Promise { + try { + return (await lookup()).status !== 404; + } catch { + return true; + } +} + +export async function profileRequiresEncryption( + env: Env, + orgId: string | undefined, + userId: string +): Promise { + if (!orgId) return env.AUTH_MODE !== "dev"; + return vaultIsActive(() => { + const stub = env.VAULT_ROOMS.get(env.VAULT_ROOMS.idFromName(`vault1/${orgId}/${userId}`)); + return stub.fetch("https://vault/", { headers: { [AUTH_USER_HEADER]: userId } }); + }); +} diff --git a/edge/src/vault-records.test.ts b/edge/src/vault-records.test.ts new file mode 100644 index 000000000..5f99182d5 --- /dev/null +++ b/edge/src/vault-records.test.ts @@ -0,0 +1,169 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { + applyGenesis, + applyMembership, + decodeBase64, + DeviceStatus, + hex, + MAX_ENVELOPE_BYTES, + parseEnvelopeHeader, + parseSignedRecord, + looksLikeSealedContent, + looksLikeSealedField, + encodeBase64, + pairingCode, + RecordKind, + RecipientKind, + recoveryAuthorityId, + unhex, + verifyEnrollmentProof, + type MembershipHead +} from "./vault-records"; + +/** Rust-generated fixture (`crates/crypto/tests/vault_fixture.rs`): the same + * bytes every verifier must accept, plus the hashes they must agree on. */ +const fixture = JSON.parse( + readFileSync(new URL("../../crates/crypto/tests/fixtures/vault.json", import.meta.url), "utf8") +) as { + vaultId: string; + generation: string; + profileHash: string; + recoverySigningKey: string; + recoveryAuthorityId: string; + deviceA: { id: string; signingKey: string }; + deviceB: { id: string }; + membership: string[]; + membershipHashes: string[]; + epochsAfter: number[]; + keyringEnvelopeB: string; + objectId: string; + objectKeyEnvelope: string; + chatRecord: string; + enrollment: { + requestId: string; + deviceId: string; + signingKey: string; + encryptionKey: string; + proof: string; + pairingCode: string; + }; +}; + +const records = fixture.membership.map((r) => decodeBase64(r)!); + +const chain = async (): Promise => { + const heads: MembershipHead[] = []; + const genesis = await applyGenesis(records[0]!, unhex(fixture.profileHash)!); + if (!genesis.ok) throw new Error(genesis.error); + let head: MembershipHead = { + sequence: 0n, + hash: genesis.hash, + epoch: 1n, + payload: genesis.payload, + vaultId: genesis.record.binding.vaultId, + generation: genesis.record.binding.generation + }; + heads.push(head); + for (const record of records.slice(1)) { + const next = await applyMembership(head, record); + if (!next.ok) throw new Error(next.error); + head = { ...head, sequence: next.payload.sequence, hash: next.hash, epoch: next.epoch, payload: next.payload }; + heads.push(head); + } + return heads; +}; + +describe("vault records (shared fixture)", () => { + it("verifies the membership chain and agrees on every hash and epoch", async () => { + const heads = await chain(); + expect(heads.map((h) => hex(h.hash))).toEqual(fixture.membershipHashes); + expect(heads.map((h) => Number(h.epoch))).toEqual(fixture.epochsAfter); + const final = heads[heads.length - 1]!; + const b = final.payload.devices.find((d) => hex(d.deviceId) === fixture.deviceB.id); + expect(b?.status).toBe(DeviceStatus.revoked); + expect(hex(await recoveryAuthorityId(final.payload.recoverySigningKey))).toBe( + fixture.recoveryAuthorityId + ); + expect(hex(final.vaultId)).toBe(fixture.vaultId); + expect(hex(final.generation)).toBe(fixture.generation); + }); + + it("rejects replays, forks, tampering, and the wrong profile", async () => { + const heads = await chain(); + const [genesisHead, addedHead] = [heads[0]!, heads[1]!]; + // Replay on a moved head. + expect((await applyMembership(addedHead, records[1]!)).ok).toBe(false); + // Revoke record built on the add-head cannot follow genesis directly. + expect((await applyMembership(genesisHead, records[2]!)).ok).toBe(false); + // Any single-bit flip fails closed. + for (const index of [0, 5, 40, 120, records[1]!.length - 1]) { + const damaged = new Uint8Array(records[1]!); + damaged[index] = damaged[index]! ^ 1; + expect((await applyMembership(genesisHead, damaged)).ok).toBe(false); + } + const wrongProfile = await applyGenesis(records[0]!, new Uint8Array(32)); + expect(wrongProfile).toEqual({ ok: false, error: "wrong_profile" }); + // A non-genesis record is not a genesis. + expect((await applyGenesis(records[1]!)).ok).toBe(false); + }); + + it("parses envelope headers and content wrappers without trusting them", () => { + const keyring = parseSignedRecord(decodeBase64(fixture.keyringEnvelopeB)!, MAX_ENVELOPE_BYTES); + expect(keyring.binding.kind).toBe(RecordKind.keyEnvelope); + const header = parseEnvelopeHeader(keyring.payload); + expect(header.recipientKind).toBe(RecipientKind.device); + expect(hex(header.recipientId)).toBe(fixture.deviceB.id); + const objectKey = parseSignedRecord(decodeBase64(fixture.objectKeyEnvelope)!, MAX_ENVELOPE_BYTES); + expect(hex(objectKey.binding.objectId)).toBe(fixture.objectId); + expect(parseEnvelopeHeader(objectKey.payload).recipientKind).toBe(RecipientKind.epoch); + const chat = parseSignedRecord(decodeBase64(fixture.chatRecord)!, 2048); + expect(chat.binding.kind).toBe(RecordKind.content); + expect(hex(chat.binding.authorId)).toBe(fixture.deviceA.id); + expect(() => parseSignedRecord(decodeBase64(fixture.chatRecord)!.subarray(1), 2048)).toThrow(); + // Framing check: content records pass; policy records, plaintext, and + // truncated bytes do not. + expect(looksLikeSealedContent(decodeBase64(fixture.chatRecord)!, 2048)).toBe(true); + expect(looksLikeSealedContent(records[0]!, 65536)).toBe(false); + expect(looksLikeSealedContent(new TextEncoder().encode("{\"title\":\"plain\"}"), 2048)).toBe(false); + expect(looksLikeSealedContent(decodeBase64(fixture.chatRecord)!.subarray(0, 40), 2048)).toBe(false); + }); + + it("checks encrypted payload framing and purpose without claiming signature verification", () => { + const bytes = decodeBase64(fixture.chatRecord)!; + expect(looksLikeSealedContent(bytes, 2048, 1n)).toBe(true); + expect(looksLikeSealedContent(bytes, 2048, 4n)).toBe(false); + expect(looksLikeSealedField({ e1: fixture.chatRecord }, 2048)).toBe(false); + expect(looksLikeSealedField("plain", 2048)).toBe(false); + const payload = parseSignedRecord(bytes, 2048).payload; + const offset = bytes.findIndex((_, index) => payload.every((byte, j) => bytes[index + j] === byte)); + expect(offset).toBeGreaterThan(0); + const field = bytes.slice(); + field[offset + 6] = 4; + expect(looksLikeSealedField({ e1: encodeBase64(field) }, 2048)).toBe(true); + expect(looksLikeSealedField({ e1: encodeBase64(field), plain: "extra" }, 2048)).toBe(false); + field[offset + 4] = 99; + expect(looksLikeSealedContent(field, 2048)).toBe(false); + }); + + it("verifies enrollment proofs and derives the same pairing code", async () => { + const request = { + vaultId: unhex(fixture.vaultId, 16)!, + requestId: unhex(fixture.enrollment.requestId, 16)!, + deviceId: unhex(fixture.enrollment.deviceId, 16)!, + signingKey: unhex(fixture.enrollment.signingKey, 32)!, + encryptionKey: unhex(fixture.enrollment.encryptionKey, 32)! + }; + const proof = unhex(fixture.enrollment.proof, 64)!; + expect(await verifyEnrollmentProof(request, proof)).toBe(true); + const genesis = unhex(fixture.membershipHashes[0]!, 32)!; + expect(await pairingCode(request, genesis)).toBe(fixture.enrollment.pairingCode); + const swapped = { ...request, encryptionKey: new Uint8Array(32).fill(9) }; + expect(await verifyEnrollmentProof(swapped, proof)).toBe(false); + expect(await pairingCode(swapped, genesis)).not.toBe(fixture.enrollment.pairingCode); + expect(await pairingCode(request, new Uint8Array(32))).not.toBe(fixture.enrollment.pairingCode); + // The identity point is not an acceptable signing key. + const identity = { ...request, signingKey: (() => { const b = new Uint8Array(32); b[0] = 1; return b; })() }; + expect(await verifyEnrollmentProof(identity, proof)).toBe(false); + }); +}); diff --git a/edge/src/vault-records.ts b/edge/src/vault-records.ts new file mode 100644 index 000000000..1b35c9571 --- /dev/null +++ b/edge/src/vault-records.ts @@ -0,0 +1,692 @@ +/** + * Vault control-plane record codec — the TypeScript twin of the fixed + * deterministic-CBOR grammar in `crates/crypto/src/{record,policy,envelope}.rs` + * (RFC 0001 §7.5, §5). The edge never holds a private key: it parses signed + * wrappers, verifies Ed25519 signatures with WebCrypto against keys taken + * from the membership head it already stores, and enforces the structural + * transition rules so a stale, forked, or forged record cannot enter the + * history. Clients re-verify everything independently. + * + * This is NOT a general CBOR decoder: it accepts exactly the shapes the Rust + * side emits and rejects everything else (duplicates, reordering, indefinite + * lengths, tags, non-shortest integers, trailing bytes). + */ + +export const RECORD_DOMAIN = "zeron/signed-record/v1\0"; +const MEMBERSHIP_DOMAIN = "zeron/membership/v1\0"; +const RECOVERY_ID_DOMAIN = "zeron/recovery-id/v1\0"; +const ENROLL_DOMAIN = "zeron/enroll/v1\0"; +const PAIRING_DOMAIN = "zeron/pairing-code/v1\0"; +export const MAX_RECORD_OVERHEAD = 256; +export const MAX_POLICY_BYTES = 64 * 1024; +export const MAX_ENVELOPE_BYTES = 16 + 1024 * 44 + 16 + 128; +export const MAX_DEVICES = 64; +export const POLICY_OBJECT_ID = new Uint8Array(16); + +export const RecordKind = { policy: 1n, keyEnvelope: 2n, content: 3n } as const; +export const Operation = { + genesis: 1n, + addDevice: 2n, + revokeDevice: 3n, + rotateRecovery: 4n, + recoveryTransition: 5n +} as const; +export const DeviceStatus = { active: 0n, revoked: 1n } as const; +export const RecipientKind = { device: 1n, recovery: 2n, epoch: 3n } as const; + +export class RecordError extends Error { + constructor(readonly code: string) { + super(code); + } +} + +const textEncoder = new TextEncoder(); + +export interface RecordBinding { + kind: bigint; + vaultId: Uint8Array; + generation: Uint8Array; + epoch: bigint; + objectId: Uint8Array; + authorId: Uint8Array; + membershipHash: Uint8Array; +} + +export interface SignedRecord { + binding: RecordBinding; + revisionId: Uint8Array; + payload: Uint8Array; + signature: Uint8Array; + /** Domain || deterministic map of fields 0..9 — what the signature covers. */ + signingInput: Uint8Array; + encoded: Uint8Array; +} + +export const bytesEqual = (a: Uint8Array, b: Uint8Array): boolean => { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!; + return diff === 0; +}; + +export const hex = (bytes: Uint8Array): string => + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); + +export const unhex = (text: string, length?: number): Uint8Array | undefined => { + if (!/^[0-9a-f]*$/i.test(text) || text.length % 2 !== 0) return undefined; + if (length !== undefined && text.length !== length * 2) return undefined; + const out = new Uint8Array(text.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16); + return out; +}; + +export const encodeBase64 = (bytes: Uint8Array): string => { + let bin = ""; + for (let i = 0; i < bytes.length; i += 0x8000) { + bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + } + return btoa(bin); +}; + +export const decodeBase64 = (text: string): Uint8Array | undefined => { + try { + const bin = atob(text); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; + } catch { + return undefined; + } +}; + +export const concat = (...parts: Uint8Array[]): Uint8Array => { + const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); + let off = 0; + for (const p of parts) { + out.set(p, off); + off += p.length; + } + return out; +}; + +const ascii = (text: string): Uint8Array => textEncoder.encode(text); + +/** Cursor over the fixed grammar; every method throws `RecordError`. */ +export class Reader { + private offset = 0; + constructor(private readonly bytes: Uint8Array) {} + + get position(): number { + return this.offset; + } + + get atEnd(): boolean { + return this.offset >= this.bytes.length; + } + + finish(): void { + if (!this.atEnd) throw new RecordError("malformed"); + } + + private take(length: number): Uint8Array { + if (length > this.bytes.length - this.offset) throw new RecordError("malformed"); + const out = this.bytes.subarray(this.offset, this.offset + length); + this.offset += length; + return out; + } + + argument(major: number): bigint { + const head = this.take(1)[0]!; + if (head >> 5 !== major) throw new RecordError("malformed"); + const additional = head & 31; + if (additional < 24) return BigInt(additional); + let length: number; + let minimum: bigint; + switch (additional) { + case 24: + length = 1; + minimum = 24n; + break; + case 25: + length = 2; + minimum = 0x100n; + break; + case 26: + length = 4; + minimum = 0x10000n; + break; + case 27: + length = 8; + minimum = 0x100000000n; + break; + default: + throw new RecordError("malformed"); + } + let value = 0n; + for (const byte of this.take(length)) value = (value << 8n) | BigInt(byte); + if (value < minimum) throw new RecordError("non_canonical"); + return value; + } + + private key(expected: number): void { + if (this.argument(0) !== BigInt(expected)) throw new RecordError("malformed"); + } + + uintField(key: number): bigint { + this.key(key); + return this.argument(0); + } + + bytesField(key: number, limit: number): Uint8Array { + this.key(key); + const length = this.argument(2); + if (length > BigInt(limit)) throw new RecordError("size_limit_exceeded"); + return this.take(Number(length)); + } + + fixedField(key: number, count: number): Uint8Array { + const value = this.bytesField(key, count); + if (value.length !== count) throw new RecordError("malformed"); + return value; + } + + fixedBytes(count: number): Uint8Array { + const length = this.argument(2); + if (length !== BigInt(count)) throw new RecordError("malformed"); + return this.take(count); + } +} + +/** Parse a signed wrapper (no signature check). */ +export const parseSignedRecord = (encoded: Uint8Array, maxPayloadBytes: number): SignedRecord => { + if (encoded.length > maxPayloadBytes + MAX_RECORD_OVERHEAD) { + throw new RecordError("size_limit_exceeded"); + } + const reader = new Reader(encoded); + if (reader.argument(5) !== 11n) throw new RecordError("malformed"); + if (reader.uintField(0) !== 1n) throw new RecordError("unsupported_version"); + const kind = reader.uintField(1); + if (kind < 1n || kind > 3n) throw new RecordError("unsupported_kind"); + const vaultId = reader.fixedField(2, 16); + const generation = reader.fixedField(3, 16); + const epoch = reader.uintField(4); + if (epoch === 0n) throw new RecordError("invalid_epoch"); + const objectId = reader.fixedField(5, 16); + const authorId = reader.fixedField(6, 16); + const revisionId = reader.fixedField(7, 16); + const membershipHash = reader.fixedField(8, 32); + const payload = reader.bytesField(9, maxPayloadBytes); + const signedEnd = reader.position; + const signature = reader.fixedField(10, 64); + reader.finish(); + const signingInput = concat( + ascii(RECORD_DOMAIN), + new Uint8Array([0xaa]), + encoded.subarray(1, signedEnd) + ); + return { + binding: { kind, vaultId, generation, epoch, objectId, authorId, membershipHash }, + revisionId, + payload, + signature, + signingInput, + encoded + }; +}; + +export interface DeviceEntry { + deviceId: Uint8Array; + signingKey: Uint8Array; + encryptionKey: Uint8Array; + status: bigint; +} + +export interface PolicyPayload { + sequence: bigint; + parentHash: Uint8Array; + profileHash: Uint8Array; + epoch: bigint; + operation: bigint; + recoverySigningKey: Uint8Array; + recoveryEncryptionKey: Uint8Array; + devices: DeviceEntry[]; +} + +export const parsePolicyPayload = (bytes: Uint8Array): PolicyPayload => { + const reader = new Reader(bytes); + if (reader.argument(5) !== 9n) throw new RecordError("malformed"); + if (reader.uintField(0) !== 1n) throw new RecordError("unsupported_version"); + const sequence = reader.uintField(1); + const parentHash = reader.fixedField(2, 32); + const profileHash = reader.fixedField(3, 32); + const epoch = reader.uintField(4); + const operation = reader.uintField(5); + if (operation < 1n || operation > 5n) throw new RecordError("unsupported_operation"); + const recoverySigningKey = reader.fixedField(6, 32); + const recoveryEncryptionKey = reader.fixedField(7, 32); + if (reader.argument(0) !== 8n) throw new RecordError("malformed"); + const count = reader.argument(4); + if (count > BigInt(MAX_DEVICES)) throw new RecordError("too_many_devices"); + const devices: DeviceEntry[] = []; + for (let i = 0n; i < count; i++) { + if (reader.argument(4) !== 4n) throw new RecordError("malformed"); + const deviceId = reader.fixedBytes(16); + const signingKey = reader.fixedBytes(32); + const encryptionKey = reader.fixedBytes(32); + const status = reader.argument(0); + if (status !== 0n && status !== 1n) throw new RecordError("malformed"); + devices.push({ deviceId, signingKey, encryptionKey, status }); + } + reader.finish(); + return { + sequence, + parentHash, + profileHash, + epoch, + operation, + recoverySigningKey, + recoveryEncryptionKey, + devices + }; +}; + +export interface EnvelopeHeader { + recipientKind: bigint; + recipientId: Uint8Array; + encapsulation: Uint8Array; + ciphertextLength: number; +} + +export const parseEnvelopeHeader = (bytes: Uint8Array): EnvelopeHeader => { + const reader = new Reader(bytes); + if (reader.argument(5) !== 5n) throw new RecordError("unsupported_format"); + if (reader.uintField(0) !== 1n) throw new RecordError("unsupported_format"); + const recipientKind = reader.uintField(1); + if (recipientKind < 1n || recipientKind > 3n) throw new RecordError("unsupported_format"); + const recipientId = reader.fixedField(2, 16); + const encapsulation = reader.fixedField(3, 32); + const ciphertext = reader.bytesField(4, MAX_ENVELOPE_BYTES); + reader.finish(); + if (ciphertext.length < 16) throw new RecordError("unsupported_format"); + return { recipientKind, recipientId, encapsulation, ciphertextLength: ciphertext.length }; +}; + +export const epochRecipientId = (epoch: bigint): Uint8Array => { + const out = new Uint8Array(16); + let value = epoch; + for (let i = 15; i >= 8; i--) { + out[i] = Number(value & 0xffn); + value >>= 8n; + } + return out; +}; + +/** Protocol-framing check (RFC 0001 §12.2): does `bytes` parse as a signed + * wrapper of kind = content? No signature or membership check — only the + * relay's refusal to store bytes that are not even ciphertext-shaped in an + * encrypted room. `maxPayloadBytes` bounds the parse. */ +export const looksLikeSealedContent = (bytes: Uint8Array, maxPayloadBytes: number, expectedPurpose?: bigint): boolean => { + try { + const record = parseSignedRecord(bytes, maxPayloadBytes); + if (record.binding.kind !== RecordKind.content) return false; + const reader = new Reader(record.payload); + if (reader.argument(5) !== 6n || reader.uintField(0) !== 1n || reader.uintField(1) !== 1n) return false; + const purpose = reader.uintField(2); + if (purpose < 1n || purpose > 9n || (expectedPurpose !== undefined && purpose !== expectedPurpose)) return false; + reader.fixedField(3, 16); + reader.fixedField(4, 32); + const ciphertext = reader.bytesField(5, maxPayloadBytes); + reader.finish(); + return ciphertext.length >= 16; + } catch { + return false; + } +}; + +const looksLikeSealedValue = (value: unknown, maxPayloadBytes: number, purpose: bigint): boolean => { + if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).length !== 1) return false; + if (!("e1" in value) || typeof value.e1 !== "string" || value.e1.length > (maxPayloadBytes + MAX_RECORD_OVERHEAD) * 2) return false; + const bytes = decodeBase64(value.e1); + return bytes !== undefined && looksLikeSealedContent(bytes, maxPayloadBytes, purpose); +}; + +/** A registry field value `{"e1": base64(record)}` framed as purpose 4. */ +export const looksLikeSealedField = (value: unknown, maxPayloadBytes: number): boolean => + looksLikeSealedValue(value, maxPayloadBytes, 4n); + +/** A registry row lifecycle proof `{"e1": base64(record)}` framed as purpose 9 + * (RFC 0001 §9): a delete in an encrypted generation must carry one. */ +export const looksLikeSealedLifecycle = (value: unknown, maxPayloadBytes: number): boolean => + looksLikeSealedValue(value, maxPayloadBytes, 9n); + +// ── digests and signatures ────────────────────────────────────────────────── + +export const sha256 = async (...parts: Uint8Array[]): Promise => + new Uint8Array(await crypto.subtle.digest("SHA-256", concat(...parts))); + +export const membershipHash = (record: Uint8Array): Promise => + sha256(ascii(MEMBERSHIP_DOMAIN), record); + +export const recoveryAuthorityId = async (signingKey: Uint8Array): Promise => + (await sha256(ascii(RECOVERY_ID_DOMAIN), signingKey)).subarray(0, 16); + +const FIELD_MODULUS = (() => { + const b = new Uint8Array(32).fill(0xff); + b[0] = 0xed; + b[31] = 0x7f; + return b; +})(); +const SMALL_ORDER_Y = [ + new Uint8Array(32), + (() => { + const b = new Uint8Array(32); + b[0] = 1; + return b; + })(), + (() => { + const b = new Uint8Array(FIELD_MODULUS); + b[0] = 0xec; + return b; + })(), + unhex("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05")!, + unhex("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a")! +]; +const SCALAR_ORDER = unhex("edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010")!; + +/** Little-endian a < b for 32-byte encodings. */ +const lessThanLe = (a: Uint8Array, b: Uint8Array): boolean => { + for (let i = 31; i >= 0; i--) { + if (a[i]! !== b[i]!) return a[i]! < b[i]!; + } + return false; +}; + +/** RFC 0001 §7.6 point-encoding precheck (public data only). */ +export const ed25519PointPrecheck = (encoded: Uint8Array): boolean => { + if (encoded.length !== 32) return false; + const y = new Uint8Array(encoded); + y[31] = y[31]! & 0x7f; + if (!lessThanLe(y, FIELD_MODULUS)) return false; + return !SMALL_ORDER_Y.some((small) => bytesEqual(small, y)); +}; + +export const ed25519ScalarPrecheck = (encoded: Uint8Array): boolean => + encoded.length === 32 && lessThanLe(encoded, SCALAR_ORDER); + +export const verifyEd25519 = async ( + publicKey: Uint8Array, + message: Uint8Array, + signature: Uint8Array +): Promise => { + if ( + publicKey.length !== 32 || + signature.length !== 64 || + !ed25519PointPrecheck(publicKey) || + !ed25519PointPrecheck(signature.subarray(0, 32)) || + !ed25519ScalarPrecheck(signature.subarray(32)) + ) { + return false; + } + try { + const key = await crypto.subtle.importKey("raw", publicKey, { name: "Ed25519" }, false, [ + "verify" + ]); + return await crypto.subtle.verify({ name: "Ed25519" }, key, signature, message); + } catch { + return false; + } +}; + +// ── membership transitions (mirror of policy.rs MembershipState) ──────────── + +export interface MembershipHead { + sequence: bigint; + hash: Uint8Array; + epoch: bigint; + payload: PolicyPayload; + vaultId: Uint8Array; + generation: Uint8Array; +} + +export type ApplyOutcome = + | { ok: true; record: SignedRecord; payload: PolicyPayload; hash: Uint8Array; epoch: bigint } + | { ok: false; error: string }; + +const validDeviceEntries = (devices: DeviceEntry[]): boolean => { + if (devices.length === 0 || devices.length > MAX_DEVICES) return false; + for (let i = 0; i < devices.length; i++) { + const device = devices[i]!; + if ( + !ed25519PointPrecheck(device.signingKey) || + device.encryptionKey.every((b) => b === 0) || + bytesEqual(device.deviceId, POLICY_OBJECT_ID) + ) { + return false; + } + for (let j = 0; j < i; j++) { + const other = devices[j]!; + if ( + bytesEqual(other.deviceId, device.deviceId) || + bytesEqual(other.signingKey, device.signingKey) || + bytesEqual(other.encryptionKey, device.encryptionKey) + ) { + return false; + } + } + } + return true; +}; + +const validRecoveryKeys = (payload: PolicyPayload): boolean => + ed25519PointPrecheck(payload.recoverySigningKey) && + !payload.recoveryEncryptionKey.every((b) => b === 0) && + !payload.devices.some( + (d) => + bytesEqual(d.signingKey, payload.recoverySigningKey) || + bytesEqual(d.encryptionKey, payload.recoveryEncryptionKey) + ); + +const sameDevice = (a: DeviceEntry, b: DeviceEntry): boolean => + bytesEqual(a.deviceId, b.deviceId) && + bytesEqual(a.signingKey, b.signingKey) && + bytesEqual(a.encryptionKey, b.encryptionKey); + +const sameEntry = (a: DeviceEntry, b: DeviceEntry): boolean => + sameDevice(a, b) && a.status === b.status; + +/** Genesis: self-signed by the single listed device, sequence 0, epoch 1. */ +export const applyGenesis = async ( + encoded: Uint8Array, + expectedProfileHash?: Uint8Array +): Promise => { + let record: SignedRecord; + let payload: PolicyPayload; + try { + record = parseSignedRecord(encoded, MAX_POLICY_BYTES); + payload = parsePolicyPayload(record.payload); + } catch (err) { + return { ok: false, error: err instanceof RecordError ? err.code : "malformed" }; + } + const { binding } = record; + if (binding.kind !== RecordKind.policy || !bytesEqual(binding.objectId, POLICY_OBJECT_ID)) { + return { ok: false, error: "wrong_kind" }; + } + if (payload.operation !== Operation.genesis) return { ok: false, error: "invalid_transition" }; + if (payload.sequence !== 0n || !payload.parentHash.every((b) => b === 0)) { + return { ok: false, error: "wrong_sequence" }; + } + if (expectedProfileHash && !bytesEqual(payload.profileHash, expectedProfileHash)) { + return { ok: false, error: "wrong_profile" }; + } + if (payload.epoch !== 1n || binding.epoch !== 1n) return { ok: false, error: "wrong_epoch" }; + if (!binding.membershipHash.every((b) => b === 0)) return { ok: false, error: "wrong_parent" }; + if (!validDeviceEntries(payload.devices) || payload.devices.length !== 1) { + return { ok: false, error: "invalid_device_set" }; + } + if (!validRecoveryKeys(payload)) return { ok: false, error: "invalid_recovery_keys" }; + const device = payload.devices[0]!; + if (device.status !== DeviceStatus.active || !bytesEqual(binding.authorId, device.deviceId)) { + return { ok: false, error: "invalid_device_set" }; + } + if (!(await verifyEd25519(device.signingKey, record.signingInput, record.signature))) { + return { ok: false, error: "invalid_signature" }; + } + return { ok: true, record, payload, hash: await membershipHash(encoded), epoch: 1n }; +}; + +/** Apply the next record on top of a verified head. */ +export const applyMembership = async ( + head: MembershipHead, + encoded: Uint8Array +): Promise => { + let record: SignedRecord; + let payload: PolicyPayload; + try { + record = parseSignedRecord(encoded, MAX_POLICY_BYTES); + payload = parsePolicyPayload(record.payload); + } catch (err) { + return { ok: false, error: err instanceof RecordError ? err.code : "malformed" }; + } + const { binding } = record; + if ( + binding.kind !== RecordKind.policy || + !bytesEqual(binding.vaultId, head.vaultId) || + !bytesEqual(binding.generation, head.generation) || + !bytesEqual(binding.objectId, POLICY_OBJECT_ID) + ) { + return { ok: false, error: "wrong_vault" }; + } + if (!bytesEqual(binding.membershipHash, head.hash) || !bytesEqual(payload.parentHash, head.hash)) { + return { ok: false, error: "stale_parent" }; + } + if (payload.sequence !== head.sequence + 1n) return { ok: false, error: "wrong_sequence" }; + if (!bytesEqual(payload.profileHash, head.payload.profileHash)) { + return { ok: false, error: "wrong_profile" }; + } + if (!validDeviceEntries(payload.devices)) return { ok: false, error: "invalid_device_set" }; + if (!validRecoveryKeys(payload)) return { ok: false, error: "invalid_recovery_keys" }; + + let signingKey: Uint8Array; + if (payload.operation === Operation.genesis) return { ok: false, error: "invalid_transition" }; + if (payload.operation === Operation.recoveryTransition) { + const authority = await recoveryAuthorityId(head.payload.recoverySigningKey); + if (!bytesEqual(binding.authorId, authority)) return { ok: false, error: "unknown_author" }; + signingKey = head.payload.recoverySigningKey; + } else { + const author = head.payload.devices.find((d) => bytesEqual(d.deviceId, binding.authorId)); + if (!author) return { ok: false, error: "unknown_author" }; + if (author.status !== DeviceStatus.active) return { ok: false, error: "revoked_author" }; + signingKey = author.signingKey; + } + const expectedEpoch = payload.operation === Operation.addDevice ? head.epoch : head.epoch + 1n; + if (payload.epoch !== expectedEpoch || binding.epoch !== expectedEpoch) { + return { ok: false, error: "wrong_epoch" }; + } + const transition = checkTransition(head.payload, payload); + if (transition) return { ok: false, error: transition }; + if (!(await verifyEd25519(signingKey, record.signingInput, record.signature))) { + return { ok: false, error: "invalid_signature" }; + } + return { ok: true, record, payload, hash: await membershipHash(encoded), epoch: payload.epoch }; +}; + +const checkTransition = (previous: PolicyPayload, next: PolicyPayload): string | undefined => { + const recoveryUnchanged = + bytesEqual(next.recoverySigningKey, previous.recoverySigningKey) && + bytesEqual(next.recoveryEncryptionKey, previous.recoveryEncryptionKey); + const recoveryReplaced = + !bytesEqual(next.recoverySigningKey, previous.recoverySigningKey) && + !bytesEqual(next.recoveryEncryptionKey, previous.recoveryEncryptionKey); + const prefixOk = (accept: (a: DeviceEntry, b: DeviceEntry) => boolean): boolean => + next.devices.length >= previous.devices.length && + previous.devices.every((p, i) => accept(p, next.devices[i]!)); + const added = next.devices.slice(previous.devices.length); + switch (next.operation) { + case Operation.addDevice: + if (!recoveryUnchanged) return "invalid_recovery_keys"; + if (!prefixOk(sameEntry)) return "invalid_device_set"; + if (added.length !== 1 || added[0]!.status !== DeviceStatus.active) return "invalid_device_set"; + return undefined; + case Operation.revokeDevice: { + if (!recoveryUnchanged) return "invalid_recovery_keys"; + if (next.devices.length !== previous.devices.length) return "invalid_device_set"; + let revoked = 0; + const ok = prefixOk((p, n) => { + if (sameEntry(p, n)) return true; + if (sameDevice(p, n) && p.status === DeviceStatus.active && n.status === DeviceStatus.revoked) { + revoked++; + return true; + } + return false; + }); + return ok && revoked === 1 ? undefined : "invalid_device_set"; + } + case Operation.rotateRecovery: + if (!recoveryReplaced) return "invalid_recovery_keys"; + if ( + next.devices.length !== previous.devices.length || + !previous.devices.every((p, i) => sameEntry(p, next.devices[i]!)) + ) { + return "invalid_device_set"; + } + return undefined; + case Operation.recoveryTransition: + if (!(recoveryUnchanged || recoveryReplaced)) return "invalid_recovery_keys"; + if ( + !prefixOk((p, n) => sameEntry(p, n) || (sameDevice(p, n) && n.status === DeviceStatus.revoked)) + ) { + return "invalid_device_set"; + } + if (added.length !== 1 || added[0]!.status !== DeviceStatus.active) return "invalid_device_set"; + return undefined; + default: + return "unsupported_operation"; + } +}; + +// ── enrollment ────────────────────────────────────────────────────────────── + +export interface EnrollmentRequest { + vaultId: Uint8Array; + requestId: Uint8Array; + deviceId: Uint8Array; + signingKey: Uint8Array; + encryptionKey: Uint8Array; +} + +const enrollmentBody = (request: EnrollmentRequest): Uint8Array => + concat( + request.vaultId, + request.requestId, + request.deviceId, + request.signingKey, + request.encryptionKey + ); + +export const verifyEnrollmentProof = async ( + request: EnrollmentRequest, + proof: Uint8Array +): Promise => { + if ( + !ed25519PointPrecheck(request.signingKey) || + request.encryptionKey.every((b) => b === 0) || + bytesEqual(request.deviceId, POLICY_OBJECT_ID) + ) { + return false; + } + return verifyEd25519( + request.signingKey, + concat(ascii(ENROLL_DOMAIN), enrollmentBody(request)), + proof + ); +}; + +/** "NNNN-NNNN" comparison code — same derivation as `policy.rs`. The + * genesis hash is part of the input so a relay that presents the pending + * device with a substitute vault produces a code the approver will not see. */ +export const pairingCode = async (request: EnrollmentRequest, genesisHash: Uint8Array): Promise => { + const digest = await sha256(ascii(PAIRING_DOMAIN), enrollmentBody(request), genesisHash); + const value = + (((digest[0]! << 24) | (digest[1]! << 16) | (digest[2]! << 8) | digest[3]!) >>> 0) % 100_000_000; + return `${String(Math.floor(value / 10_000)).padStart(4, "0")}-${String(value % 10_000).padStart(4, "0")}`; +}; diff --git a/edge/src/vault-room.ts b/edge/src/vault-room.ts new file mode 100644 index 000000000..355a7a7eb --- /dev/null +++ b/edge/src/vault-room.ts @@ -0,0 +1,628 @@ +/** + * VaultRoom — the per-profile encrypted-sync control plane (RFC 0001 §6), + * one Durable Object per `(orgId, userId)` (`vault1/{orgId}/{userId}`). + * + * It stores only public and encrypted material: the signed membership + * history, HPKE keyring envelopes addressed to devices / the recovery + * authority, per-object wrapped content keys, and short-lived enrollment + * requests. There is no unlock endpoint, no administrative key, and no + * plaintext keyring anywhere in this class. Every write is authenticated by + * an Ed25519 signature checked against the membership head the room already + * holds (or a self-signature for genesis / enrollment proofs); the account + * bearer alone can read metadata but cannot alter trust. + * + * Routes (after the Worker's org check and user stamp): + * GET / descriptor (404 when no vault exists) + * GET /membership?after=N signed policy records after sequence N + * POST /membership append one signed record (CAS on parent) + * GET /envelopes recipient ids + epochs + * GET /envelopes/:recipient latest keyring envelope for a recipient + * PUT /envelopes/:recipient publish a keyring envelope (signed) + * GET /objects/:object/keys wrapped content keys for an object + * PUT /objects/:object/keys publish one wrapped key (first writer wins) + * GET /enroll pending enrollment requests + * POST /enroll create a request (device-possession proof) + * GET /enroll/:request request status + * POST /enroll/:request/approve mark approved (after membership + envelope) + * POST /enroll/:request/reject reject / cancel + */ +import { AUTH_USER_HEADER, type Env } from "./env"; +import { + applyGenesis, + applyMembership, + bytesEqual, + encodeBase64, + epochRecipientId, + hex, + MAX_ENVELOPE_BYTES, + MAX_POLICY_BYTES, + MAX_RECORD_OVERHEAD, + parseEnvelopeHeader, + parsePolicyPayload, + parseSignedRecord, + pairingCode, + POLICY_OBJECT_ID, + RecipientKind, + RecordError, + RecordKind, + DeviceStatus, + unhex, + verifyEd25519, + verifyEnrollmentProof, + type EnrollmentRequest, + type MembershipHead, + type SignedRecord +} from "./vault-records"; + +const ENROLLMENT_TTL_MS = 15 * 60 * 1000; +const MAX_PENDING_ENROLLMENTS = 16; +const MAX_MEMBERSHIP_PAGE = 256; +const MAX_OBJECT_KEYS_PER_OBJECT = 1024; +const HEX16 = /^[0-9a-f]{32}$/; + +interface MembershipRow extends Record { + seq: number; + hash: ArrayBuffer; + epoch: string; + record: ArrayBuffer; +} + +interface EnrollmentRow extends Record { + request: ArrayBuffer; + device: ArrayBuffer; + signing: ArrayBuffer; + encryption: ArrayBuffer; + created_at: number; + expires_at: number; + status: string; + membership_seq: number | null; +} + +export class VaultRoom implements DurableObject { + private readonly ctx: DurableObjectState; + + constructor(ctx: DurableObjectState, _env: Env) { + this.ctx = ctx; + ctx.storage.sql.exec( + `CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS membership ( + seq INTEGER PRIMARY KEY, hash BLOB NOT NULL, author BLOB NOT NULL, + epoch TEXT NOT NULL, record BLOB NOT NULL, received_at INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS envelopes ( + recipient BLOB PRIMARY KEY, kind INTEGER NOT NULL, epoch TEXT NOT NULL, + author BLOB NOT NULL, record BLOB NOT NULL, updated_at INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS object_keys ( + object BLOB NOT NULL, epoch TEXT NOT NULL, author BLOB NOT NULL, + record BLOB NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (object, epoch)); + CREATE TABLE IF NOT EXISTS enrollments ( + request BLOB PRIMARY KEY, device BLOB NOT NULL, signing BLOB NOT NULL, + encryption BLOB NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, + status TEXT NOT NULL, membership_seq INTEGER);` + ); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const userId = request.headers.get(AUTH_USER_HEADER); + if (!userId) return json({ error: "unauthenticated" }, 401); + const parts = url.pathname.split("/").filter(Boolean); + const method = request.method; + + if (parts.length === 0) { + return method === "GET" ? this.descriptor() : methodNotAllowed(); + } + if (parts[0] === "membership" && parts.length === 1) { + if (method === "GET") return this.membershipPage(url); + if (method === "POST") return this.appendMembership(request); + return methodNotAllowed(); + } + if (parts[0] === "envelopes") { + if (parts.length === 1 && method === "GET") return this.listEnvelopes(); + if (parts.length === 2 && HEX16.test(parts[1]!)) { + const recipient = unhex(parts[1]!, 16)!; + if (method === "GET") return this.getEnvelope(recipient); + if (method === "PUT") return this.putEnvelope(recipient, request); + } + return methodNotAllowed(); + } + if (parts[0] === "objects" && parts.length === 3 && HEX16.test(parts[1]!) && parts[2] === "keys") { + const object = unhex(parts[1]!, 16)!; + if (method === "GET") return this.getObjectKeys(object); + if (method === "PUT") return this.putObjectKey(object, request); + return methodNotAllowed(); + } + if (parts[0] === "enroll") { + this.expireEnrollments(); + if (parts.length === 1) { + if (method === "GET") return this.listEnrollments(); + if (method === "POST") return this.createEnrollment(request); + return methodNotAllowed(); + } + if (HEX16.test(parts[1]!)) { + const id = unhex(parts[1]!, 16)!; + if (parts.length === 2 && method === "GET") return this.enrollmentStatus(id); + if (parts.length === 3 && method === "POST" && parts[2] === "approve") { + return this.approveEnrollment(id, request); + } + if (parts.length === 3 && method === "POST" && parts[2] === "reject") { + return this.rejectEnrollment(id); + } + } + return methodNotAllowed(); + } + return json({ error: "not_found" }, 404); + } + + // ── membership ──────────────────────────────────────────────────────────── + + private get sql(): SqlStorage { + return this.ctx.storage.sql; + } + + private headRow(): MembershipRow | undefined { + return [...this.sql.exec("SELECT seq, hash, epoch, record FROM membership ORDER BY seq DESC LIMIT 1")][0]; + } + + /** The verified head, rebuilt from the stored head record (records are + * only stored after verification, so re-parsing is not re-trusting). */ + private head(): MembershipHead | undefined { + const row = this.headRow(); + if (!row) return undefined; + const record = parseSignedRecord(new Uint8Array(row.record), MAX_POLICY_BYTES); + const payload = parsePolicyPayload(record.payload); + return { + sequence: BigInt(row.seq), + hash: new Uint8Array(row.hash), + epoch: BigInt(row.epoch), + payload, + vaultId: record.binding.vaultId, + generation: record.binding.generation + }; + } + + private headMatches(hash: Uint8Array): boolean { + const current = this.headRow(); + return current !== undefined && bytesEqual(hash, new Uint8Array(current.hash)); + } + + private descriptor(): Response { + const head = this.head(); + if (!head) return json({ error: "not_found" }, 404); + return json({ + vaultId: hex(head.vaultId), + generation: hex(head.generation), + headSeq: Number(head.sequence), + headHash: hex(head.hash), + genesisHash: hex(this.genesisHash()), + activeEpoch: head.epoch.toString(), + profileHash: hex(head.payload.profileHash), + activeDevices: head.payload.devices.filter((d) => d.status === DeviceStatus.active).length, + devices: head.payload.devices.map((d) => ({ + deviceId: hex(d.deviceId), + status: d.status === DeviceStatus.active ? "active" : "revoked" + })) + }); + } + + private membershipPage(url: URL): Response { + const after = Number(url.searchParams.get("after") ?? "-1"); + if (!Number.isInteger(after) || after < -1) return json({ error: "bad_after" }, 400); + const rows = [ + ...this.sql.exec( + "SELECT seq, hash, epoch, record FROM membership WHERE seq > ? ORDER BY seq LIMIT ?", + after, + MAX_MEMBERSHIP_PAGE + 1 + ) + ]; + const head = this.headRow(); + const page = rows.slice(0, MAX_MEMBERSHIP_PAGE); + return json({ + records: page.map((r) => encodeBase64(new Uint8Array(r.record))), + hashes: page.map((r) => hex(new Uint8Array(r.hash))), + truncated: rows.length > MAX_MEMBERSHIP_PAGE, + headSeq: head ? head.seq : -1, + headHash: head ? hex(new Uint8Array(head.hash)) : null + }); + } + + private async appendMembership(request: Request): Promise { + const body = await readBody(request, MAX_POLICY_BYTES + MAX_RECORD_OVERHEAD); + if (!body) return json({ error: "too_large" }, 413); + const head = this.head(); + const outcome = head ? await applyMembership(head, body) : await applyGenesis(body); + if (!outcome.ok) { + const status = outcome.error === "stale_parent" || outcome.error === "wrong_sequence" ? 409 : 400; + return json( + { + error: outcome.error, + headSeq: head ? Number(head.sequence) : -1, + headHash: head ? hex(head.hash) : null + }, + status + ); + } + const current = this.headRow(); + if (head ? !current || !bytesEqual(head.hash, new Uint8Array(current.hash)) : current !== undefined) { + return json({ error: "stale_parent" }, 409); + } + // Genesis may only ever create; a second genesis on an existing vault + // cannot reset trust (applyMembership already refuses op=genesis). + this.sql.exec( + "INSERT INTO membership (seq, hash, author, epoch, record, received_at) VALUES (?, ?, ?, ?, ?, ?)", + Number(outcome.payload.sequence), + buffer(outcome.hash), + buffer(outcome.record.binding.authorId), + outcome.epoch.toString(), + buffer(body), + Date.now() + ); + return json({ + ok: true, + headSeq: Number(outcome.payload.sequence), + headHash: hex(outcome.hash), + activeEpoch: outcome.epoch.toString() + }); + } + + /** Parse + verify a key-envelope record signed by an active device of the + * current head. Returns the record or an error response. */ + private async verifiedEnvelope( + body: Uint8Array, + expectedObject: Uint8Array + ): Promise<{ record: SignedRecord; head: MembershipHead } | Response> { + const head = this.head(); + if (!head) return json({ error: "not_found" }, 404); + let record: SignedRecord; + try { + record = parseSignedRecord(body, MAX_ENVELOPE_BYTES); + } catch (err) { + return json({ error: err instanceof RecordError ? err.code : "malformed" }, 400); + } + const { binding } = record; + if ( + binding.kind !== RecordKind.keyEnvelope || + !bytesEqual(binding.vaultId, head.vaultId) || + !bytesEqual(binding.generation, head.generation) || + !bytesEqual(binding.objectId, expectedObject) + ) { + return json({ error: "wrong_vault" }, 400); + } + if (!bytesEqual(binding.membershipHash, head.hash)) { + return json({ error: "stale_parent", headSeq: Number(head.sequence), headHash: hex(head.hash) }, 409); + } + if (binding.epoch > head.epoch) return json({ error: "wrong_epoch" }, 400); + const author = head.payload.devices.find((d) => bytesEqual(d.deviceId, binding.authorId)); + if (!author) return json({ error: "unknown_author" }, 403); + if (author.status !== DeviceStatus.active) return json({ error: "revoked_author" }, 403); + if (!(await verifyEd25519(author.signingKey, record.signingInput, record.signature))) { + return json({ error: "invalid_signature" }, 403); + } + return { record, head }; + } + + // ── keyring envelopes ───────────────────────────────────────────────────── + + private listEnvelopes(): Response { + const rows = [ + ...this.sql.exec<{ recipient: ArrayBuffer; kind: number; epoch: string; updated_at: number }>( + "SELECT recipient, kind, epoch, updated_at FROM envelopes ORDER BY updated_at" + ) + ]; + return json({ + envelopes: rows.map((r) => ({ + recipientId: hex(new Uint8Array(r.recipient)), + recipientKind: r.kind, + epoch: r.epoch, + updatedAt: r.updated_at + })) + }); + } + + private getEnvelope(recipient: Uint8Array): Response { + const row = [ + ...this.sql.exec<{ record: ArrayBuffer; epoch: string }>( + "SELECT record, epoch FROM envelopes WHERE recipient = ?", + buffer(recipient) + ) + ][0]; + if (!row) return json({ error: "not_found" }, 404); + return octets(new Uint8Array(row.record), { "x-zeron-vault-epoch": row.epoch }); + } + + private async putEnvelope(recipient: Uint8Array, request: Request): Promise { + const body = await readBody(request, MAX_ENVELOPE_BYTES + MAX_RECORD_OVERHEAD); + if (!body) return json({ error: "too_large" }, 413); + const verified = await this.verifiedEnvelope(body, POLICY_OBJECT_ID); + if (verified instanceof Response) return verified; + const { record, head } = verified; + if (!this.headMatches(head.hash)) return json({ error: "stale_parent" }, 409); + let header; + try { + header = parseEnvelopeHeader(record.payload); + } catch (err) { + return json({ error: err instanceof RecordError ? err.code : "malformed" }, 400); + } + if (header.recipientKind === RecipientKind.epoch || !bytesEqual(header.recipientId, recipient)) { + return json({ error: "wrong_recipient" }, 400); + } + // The recipient must be a device in the head (any status: a device may + // receive its envelope in the same approval that adds it) or the + // recovery authority; unknown recipients are not a storage service. + if (header.recipientKind === RecipientKind.device) { + if (!head.payload.devices.some((d) => bytesEqual(d.deviceId, recipient))) { + return json({ error: "unknown_recipient" }, 400); + } + } + const existing = [ + ...this.sql.exec<{ epoch: string }>("SELECT epoch FROM envelopes WHERE recipient = ?", buffer(recipient)) + ][0]; + if (existing && BigInt(existing.epoch) > record.binding.epoch) { + return json({ error: "stale_epoch", epoch: existing.epoch }, 409); + } + this.sql.exec( + `INSERT INTO envelopes (recipient, kind, epoch, author, record, updated_at) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(recipient) DO UPDATE SET kind = excluded.kind, epoch = excluded.epoch, + author = excluded.author, record = excluded.record, updated_at = excluded.updated_at`, + buffer(recipient), + Number(header.recipientKind), + record.binding.epoch.toString(), + buffer(record.binding.authorId), + buffer(body), + Date.now() + ); + return json({ ok: true, epoch: record.binding.epoch.toString() }); + } + + // ── object keys ─────────────────────────────────────────────────────────── + + private getObjectKeys(object: Uint8Array): Response { + const rows = [ + ...this.sql.exec<{ epoch: string; record: ArrayBuffer }>( + "SELECT epoch, record FROM object_keys WHERE object = ? ORDER BY CAST(epoch AS INTEGER)", + buffer(object) + ) + ]; + return json({ + keys: rows.map((r) => ({ epoch: r.epoch, record: encodeBase64(new Uint8Array(r.record)) })) + }); + } + + private async putObjectKey(object: Uint8Array, request: Request): Promise { + const body = await readBody(request, MAX_ENVELOPE_BYTES + MAX_RECORD_OVERHEAD); + if (!body) return json({ error: "too_large" }, 413); + if (bytesEqual(object, POLICY_OBJECT_ID)) return json({ error: "wrong_object" }, 400); + const verified = await this.verifiedEnvelope(body, object); + if (verified instanceof Response) return verified; + const { record, head } = verified; + if (!this.headMatches(head.hash)) return json({ error: "stale_parent" }, 409); + let header; + try { + header = parseEnvelopeHeader(record.payload); + } catch (err) { + return json({ error: err instanceof RecordError ? err.code : "malformed" }, 400); + } + if ( + header.recipientKind !== RecipientKind.epoch || + !bytesEqual(header.recipientId, epochRecipientId(record.binding.epoch)) + ) { + return json({ error: "wrong_recipient" }, 400); + } + const epoch = record.binding.epoch.toString(); + const existing = [ + ...this.sql.exec<{ record: ArrayBuffer }>( + "SELECT record FROM object_keys WHERE object = ? AND epoch = ?", + buffer(object), + epoch + ) + ][0]; + if (existing) { + const stored = new Uint8Array(existing.record); + // First writer wins: a concurrent creator adopts the stored key so + // every writer of this object/epoch seals under ONE root key. + return json( + { ok: bytesEqual(stored, body), conflict: !bytesEqual(stored, body), epoch, record: encodeBase64(stored) }, + bytesEqual(stored, body) ? 200 : 409 + ); + } + const count = [ + ...this.sql.exec<{ n: number }>("SELECT COUNT(*) AS n FROM object_keys WHERE object = ?", buffer(object)) + ][0]!.n; + if (count >= MAX_OBJECT_KEYS_PER_OBJECT) return json({ error: "too_many_keys" }, 429); + this.sql.exec( + "INSERT INTO object_keys (object, epoch, author, record, created_at) VALUES (?, ?, ?, ?, ?)", + buffer(object), + epoch, + buffer(record.binding.authorId), + buffer(body), + Date.now() + ); + return json({ ok: true, conflict: false, epoch, record: encodeBase64(body) }); + } + + // ── enrollment ──────────────────────────────────────────────────────────── + + private expireEnrollments(): void { + this.sql.exec( + "UPDATE enrollments SET status = 'expired' WHERE status = 'pending' AND expires_at < ?", + Date.now() + ); + // Bound the table: keep the newest 64 non-pending rows. + this.sql.exec( + `DELETE FROM enrollments WHERE status != 'pending' AND request NOT IN + (SELECT request FROM enrollments WHERE status != 'pending' ORDER BY created_at DESC LIMIT 64)` + ); + } + + private genesisHash(): Uint8Array { + const row = [...this.sql.exec<{ hash: ArrayBuffer }>("SELECT hash FROM membership WHERE seq = 0")][0]; + return row ? new Uint8Array(row.hash) : new Uint8Array(32); + } + + private enrollmentJson(row: EnrollmentRow, vaultId: Uint8Array): Promise> { + const request: EnrollmentRequest = { + vaultId, + requestId: new Uint8Array(row.request), + deviceId: new Uint8Array(row.device), + signingKey: new Uint8Array(row.signing), + encryptionKey: new Uint8Array(row.encryption) + }; + return pairingCode(request, this.genesisHash()).then((code) => ({ + requestId: hex(request.requestId), + deviceId: hex(request.deviceId), + signingKey: hex(request.signingKey), + encryptionKey: hex(request.encryptionKey), + // The server's copy of the code is a convenience for the approving + // client's display only; each client recomputes it from the keys it + // verifies, never from this field. + pairingCode: code, + createdAt: row.created_at, + expiresAt: row.expires_at, + status: row.status, + membershipSeq: row.membership_seq + })); + } + + private async listEnrollments(): Promise { + const head = this.head(); + if (!head) return json({ error: "not_found" }, 404); + const rows = [ + ...this.sql.exec( + "SELECT * FROM enrollments WHERE status = 'pending' ORDER BY created_at" + ) + ]; + return json({ requests: await Promise.all(rows.map((r) => this.enrollmentJson(r, head.vaultId))) }); + } + + private async createEnrollment(request: Request): Promise { + const head = this.head(); + if (!head) return json({ error: "not_found" }, 404); + let body: { requestId?: string; deviceId?: string; signingKey?: string; encryptionKey?: string; proof?: string }; + try { + body = (await request.json()) as typeof body; + } catch { + return json({ error: "bad_request" }, 400); + } + const requestId = unhex(body.requestId ?? "", 16); + const deviceId = unhex(body.deviceId ?? "", 16); + const signingKey = unhex(body.signingKey ?? "", 32); + const encryptionKey = unhex(body.encryptionKey ?? "", 32); + const proof = unhex(body.proof ?? "", 64); + if (!requestId || !deviceId || !signingKey || !encryptionKey || !proof) { + return json({ error: "bad_request" }, 400); + } + const enrollment: EnrollmentRequest = { vaultId: head.vaultId, requestId, deviceId, signingKey, encryptionKey }; + if (!(await verifyEnrollmentProof(enrollment, proof))) return json({ error: "invalid_proof" }, 403); + if (head.payload.devices.some((d) => bytesEqual(d.deviceId, deviceId) || bytesEqual(d.signingKey, signingKey))) { + return json({ error: "already_member" }, 409); + } + const pending = [ + ...this.sql.exec<{ n: number }>("SELECT COUNT(*) AS n FROM enrollments WHERE status = 'pending'") + ][0]!.n; + if (pending >= MAX_PENDING_ENROLLMENTS) return json({ error: "too_many_requests" }, 429); + const existing = [ + ...this.sql.exec("SELECT * FROM enrollments WHERE request = ?", buffer(requestId)) + ][0]; + if (existing) { + if ( + !bytesEqual(new Uint8Array(existing.device), deviceId) || + !bytesEqual(new Uint8Array(existing.signing), signingKey) + ) { + return json({ error: "conflict" }, 409); + } + return json(await this.enrollmentJson(existing, head.vaultId)); + } + const now = Date.now(); + this.sql.exec( + `INSERT INTO enrollments (request, device, signing, encryption, created_at, expires_at, status, membership_seq) + VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL)`, + buffer(requestId), + buffer(deviceId), + buffer(signingKey), + buffer(encryptionKey), + now, + now + ENROLLMENT_TTL_MS + ); + const row = [...this.sql.exec("SELECT * FROM enrollments WHERE request = ?", buffer(requestId))][0]!; + return json(await this.enrollmentJson(row, head.vaultId), 201); + } + + private async enrollmentStatus(id: Uint8Array): Promise { + const head = this.head(); + if (!head) return json({ error: "not_found" }, 404); + const row = [...this.sql.exec("SELECT * FROM enrollments WHERE request = ?", buffer(id))][0]; + if (!row) return json({ error: "not_found" }, 404); + return json(await this.enrollmentJson(row, head.vaultId)); + } + + /** Approval is only a bookkeeping mark: the approver must already have + * appended the add-device membership record and published the device's + * keyring envelope, both of which are independently signed. */ + private async approveEnrollment(id: Uint8Array, request: Request): Promise { + const head = this.head(); + if (!head) return json({ error: "not_found" }, 404); + const row = [...this.sql.exec("SELECT * FROM enrollments WHERE request = ?", buffer(id))][0]; + if (!row) return json({ error: "not_found" }, 404); + if (row.status !== "pending") return json({ error: "not_pending", status: row.status }, 409); + let body: { membershipSeq?: number }; + try { + body = (await request.json()) as typeof body; + } catch { + return json({ error: "bad_request" }, 400); + } + const device = new Uint8Array(row.device); + const member = head.payload.devices.find((d) => bytesEqual(d.deviceId, device)); + if (!member || member.status !== DeviceStatus.active) return json({ error: "not_member" }, 409); + if (!bytesEqual(member.signingKey, new Uint8Array(row.signing))) return json({ error: "key_mismatch" }, 409); + const envelope = [ + ...this.sql.exec<{ epoch: string }>("SELECT epoch FROM envelopes WHERE recipient = ?", buffer(device)) + ][0]; + if (!envelope) return json({ error: "envelope_missing" }, 409); + const seq = Number.isInteger(body.membershipSeq) ? body.membershipSeq! : Number(head.sequence); + this.sql.exec( + "UPDATE enrollments SET status = 'approved', membership_seq = ? WHERE request = ?", + seq, + buffer(id) + ); + return json({ ok: true, membershipSeq: seq }); + } + + private rejectEnrollment(id: Uint8Array): Response { + const row = [...this.sql.exec("SELECT * FROM enrollments WHERE request = ?", buffer(id))][0]; + if (!row) return json({ error: "not_found" }, 404); + if (row.status !== "pending") return json({ error: "not_pending", status: row.status }, 409); + this.sql.exec("UPDATE enrollments SET status = 'rejected' WHERE request = ?", buffer(id)); + return json({ ok: true }); + } +} + +// ── helpers ───────────────────────────────────────────────────────────────── + +const json = (value: unknown, status = 200): Response => + new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json" } + }); + +const octets = (bytes: Uint8Array, extra: Record = {}): Response => + new Response(buffer(bytes), { + headers: { + "content-type": "application/octet-stream", + "content-length": String(bytes.byteLength), + "cache-control": "private, no-store", + ...extra + } + }); + +const methodNotAllowed = (): Response => json({ error: "not_found" }, 404); + +const buffer = (bytes: Uint8Array): ArrayBuffer => + bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; + +/** Read a bounded octet body; `undefined` when it exceeds `limit`. */ +const readBody = async (request: Request, limit: number): Promise => { + const declared = Number(request.headers.get("content-length") ?? "0"); + if (Number.isFinite(declared) && declared > limit) return undefined; + const bytes = new Uint8Array(await request.arrayBuffer()); + if (bytes.byteLength > limit || bytes.byteLength === 0) return undefined; + return bytes; +}; diff --git a/edge/test/workerd/env.d.ts b/edge/test/workerd/env.d.ts index 42c173928..7837cbac2 100644 --- a/edge/test/workerd/env.d.ts +++ b/edge/test/workerd/env.d.ts @@ -3,5 +3,9 @@ declare module "cloudflare:test" { interface ProvidedEnv { TEST_LOG: DurableObjectNamespace; + TEST_CHAT: DurableObjectNamespace; + TEST_REGISTRY: DurableObjectNamespace; + TEST_DEVICE: DurableObjectNamespace; + VAULT_ROOMS: DurableObjectNamespace; } } diff --git a/edge/test/workerd/fixture.ts b/edge/test/workerd/fixture.ts index 0c714fe9b..e1886f5e5 100644 --- a/edge/test/workerd/fixture.ts +++ b/edge/test/workerd/fixture.ts @@ -1,4 +1,8 @@ import { DurableObject } from "cloudflare:workers"; +export { ChatRoom } from "../../src/chat-room"; +export { RegistryRoom } from "../../src/registry-room"; +export { VaultRoom } from "../../src/vault-room"; +export { DeviceRoom } from "../../src/device-room"; /** Bare SQLite-backed DO; tests reach its real `ctx.storage.sql` via * `runInDurableObject` (the cloudflare-os TEST_OVERSEER pattern). */ diff --git a/edge/test/workerd/vault-gates.workerd.test.ts b/edge/test/workerd/vault-gates.workerd.test.ts new file mode 100644 index 000000000..e56d678eb --- /dev/null +++ b/edge/test/workerd/vault-gates.workerd.test.ts @@ -0,0 +1,188 @@ +import { env } from "cloudflare:test"; +import { describe, expect, it } from "vitest"; +import fixture from "../../../crates/crypto/tests/fixtures/vault.json"; +import { AUTH_ORG_HEADER, AUTH_USER_HEADER, ENCRYPTED_ROOM_HEADER } from "../../src/env"; +import { decodeBase64 } from "../../src/vault-records"; +import { decodeFrame, encodeFrame, FRAME } from "../../src/chat-frames"; +import { decodeDeviceFrame, encodeDeviceFrame } from "../../src/device-room"; + +function profile() { + const org = `org-${crypto.randomUUID()}`; + const user = "test-user"; + const headers = { [AUTH_USER_HEADER]: user, [AUTH_ORG_HEADER]: org }; + const vault = env.VAULT_ROOMS.get(env.VAULT_ROOMS.idFromName(`vault1/${org}/${user}`)); + return { org, headers, vault }; +} + +async function activate(vault: DurableObjectStub, headers: Record) { + const response = await vault.fetch("https://vault/membership", { + method: "POST", headers, body: decodeBase64(fixture.membership[0]!)! + }); + expect(response.status).toBe(200); +} + +function nextMessage(socket: WebSocket): Promise { + return new Promise((resolve, reject) => { + socket.addEventListener("message", (event) => resolve(event.data), { once: true }); + socket.addEventListener("close", () => reject(new Error("closed before reply")), { once: true }); + }); +} + +async function connect(stub: DurableObjectStub, headers: Record, path = "/ws") { + const response = await stub.fetch(`https://room${path}`, { headers: { ...headers, Upgrade: "websocket" } }); + expect(response.status).toBe(101); + const socket = response.webSocket!; + socket.binaryType = "arraybuffer"; + socket.accept(); + return socket; +} + +describe("vault fences on actual Durable Objects", () => { + it("fences an already-open plaintext chat socket after vault creation", async () => { + const { org, headers, vault } = profile(); + const room = env.TEST_CHAT.get(env.TEST_CHAT.idFromName(org)); + const socket = await connect(room, headers); + const hello = nextMessage(socket); + socket.send(encodeFrame(FRAME.hello, { device: "device", cursor: 0 })); + expect(decodeFrame(new Uint8Array(await hello as ArrayBuffer))?.type).toBe(FRAME.state); + await activate(vault, headers); + const rejection = nextMessage(socket); + socket.send(encodeFrame(FRAME.push, { batchId: "plain" }, new TextEncoder().encode("plaintext canary"))); + expect(decodeFrame(new Uint8Array(await rejection as ArrayBuffer))?.header.code).toBe("encrypted_profile"); + const stats = await room.fetch("https://room/stats", { headers }); + expect((await stats.json<{ rowCount: number }>()).rowCount).toBe(0); + socket.close(); + }); + + it("fences an already-open plaintext registry socket after vault creation", async () => { + const { org, headers, vault } = profile(); + const room = env.TEST_REGISTRY.get(env.TEST_REGISTRY.idFromName(org)); + const socket = await connect(room, headers); + const hello = nextMessage(socket); + socket.send(JSON.stringify({ t: "hello", device: "device", cursor: 0 })); + expect(JSON.parse(await hello as string).t).toBe("state"); + await activate(vault, headers); + const rejection = nextMessage(socket); + socket.send(JSON.stringify({ t: "push", batch: "plain", ops: [] })); + expect(JSON.parse(await rejection as string).code).toBe("encrypted_profile"); + const stats = await room.fetch("https://room/stats", { headers }); + expect((await stats.json<{ rowCount: number }>()).rowCount).toBe(0); + socket.close(); + }); + + it("preserves sealed bytes across WebSocket send and HTTPS retry/pull", async () => { + const { org, headers } = profile(); + const room = env.TEST_CHAT.get(env.TEST_CHAT.idFromName(org)); + const socket = await connect(room, { ...headers, [ENCRYPTED_ROOM_HEADER]: "1" }); + const hello = nextMessage(socket); + socket.send(encodeFrame(FRAME.hello, { device: "device", cursor: 0 })); + await hello; + const bytes = decodeBase64(fixture.chatRecord)!; + const ack = nextMessage(socket); + socket.send(encodeFrame(FRAME.push, { batchId: "sealed" }, bytes)); + expect(decodeFrame(new Uint8Array(await ack as ArrayBuffer))?.header.seq).toBe(1); + const retry = await room.fetch("https://room/rows?device=device&batchId=sealed", { + method: "POST", headers, body: bytes + }); + expect(await retry.json()).toMatchObject({ seq: 1, dup: true }); + const pull = await room.fetch("https://room/rows?after=0", { headers }); + const body = new Uint8Array(await pull.arrayBuffer()); + const view = new DataView(body.buffer); + const rows: Uint8Array[] = []; + for (let offset = 0; offset < body.length;) { + const length = view.getUint32(offset, true); + const frame = decodeFrame(body.subarray(offset + 4, offset + 4 + length))!; + if (frame.type === FRAME.row) rows.push(frame.payload); + offset += 4 + length; + } + expect(rows).toHaveLength(1); + expect(Array.from(rows[0]!)).toEqual(Array.from(bytes)); + socket.close(); + }); + + it("rejects plaintext over HTTPS in an encrypted registry generation", async () => { + const { org, headers } = profile(); + const room = env.TEST_REGISTRY.get(env.TEST_REGISTRY.idFromName(org)); + const response = await room.fetch("https://room/push", { + method: "POST", headers: { ...headers, [ENCRYPTED_ROOM_HEADER]: "1" }, + body: JSON.stringify({ batch: "plain", ops: [{ kind: "chats", id: "chat", op: "upsert", hlc: "0000000000001-000000-device", set: { title: "plaintext canary" } }] }) + }); + expect(response.status).toBe(400); + expect((await response.json<{ error: string }>()).error).toBe("plaintext_rejected"); + const stats = await room.fetch("https://room/stats", { headers }); + expect((await stats.json<{ rowCount: number }>()).rowCount).toBe(0); + }); + + it("tombstones an encrypted registry row only with a lifecycle proof", async () => { + const { org, headers } = profile(); + const room = env.TEST_REGISTRY.get(env.TEST_REGISTRY.idFromName(org)); + const push = (op: Record) => room.fetch("https://room/push", { + method: "POST", headers: { ...headers, [ENCRYPTED_ROOM_HEADER]: "1" }, + body: JSON.stringify({ batch: `b-${Math.random()}`, ops: [op] }) + }); + // A bare delete (a relay-shaped forgery) never lands. + const bare = await push({ kind: "chats", id: "chat-1", op: "delete", hlc: "0000000000002-000000-dev-a" }); + expect(bare.status).toBe(400); + expect((await bare.json<{ error: string }>()).error).toBe("plaintext_rejected"); + // A field record in the proof slot is the wrong purpose: refused too. + const wrongPurpose = await push({ kind: "chats", id: "chat-1", op: "delete", hlc: "0000000000002-000000-dev-a", proof: { e1: fixture.chatRecord } }); + expect(wrongPurpose.status).toBe(400); + // The member's sealed lifecycle proof tombstones the row and is served + // back with it, byte for byte, for readers to verify. + const proven = await push({ kind: "chats", id: "chat-1", op: "delete", hlc: "0000000000002-000000-dev-a", proof: { e1: fixture.registryLifecycleRecord } }); + expect(proven.status).toBe(200); + const rows = await room.fetch("https://room/rows?since=0", { headers }); + const body = await rows.json<{ rows: Array<{ id: string; deleted: boolean; delHlc?: string; delProof?: { e1: string } }> }>(); + expect(body.rows).toHaveLength(1); + expect(body.rows[0]).toMatchObject({ id: "chat-1", deleted: true, delHlc: "0000000000002-000000-dev-a", delProof: { e1: fixture.registryLifecycleRecord } }); + }); + + it("rejects plaintext over HTTPS in an encrypted chat generation", async () => { + const { org, headers } = profile(); + const room = env.TEST_CHAT.get(env.TEST_CHAT.idFromName(org)); + const socket = await connect(room, { ...headers, [ENCRYPTED_ROOM_HEADER]: "1" }); + const response = await room.fetch("https://room/rows?device=device&batchId=plain", { + method: "POST", headers, body: "plaintext canary" + }); + expect(response.status).toBe(400); + expect((await response.json<{ error: string }>()).error).toBe("plaintext_rejected"); + socket.close(); + }); + + it("refuses legacy device sidecars and RPC once encryption is required", async () => { + const { org, headers, vault } = profile(); + const room = env.TEST_DEVICE.get(env.TEST_DEVICE.idFromName(org)); + const socket = await connect(room, headers, "/ws?role=host"); + await activate(vault, headers); + const response = await room.fetch("https://room/sidecar/repos", { + method: "POST", headers, body: JSON.stringify({ path: "plaintext canary" }) + }); + expect(response.status).toBe(409); + const stored = await room.fetch("https://room/sidecar/repos", { headers }); + expect(stored.status).toBe(404); + // Device-channel frames (handshake + ciphertext) still cross the relay: + // with no client "peer" attached the relay bounces client_gone instead + // of closing the socket. + const relayed = new Promise((resolve) => + socket.addEventListener("message", (event) => resolve(new Uint8Array(event.data as ArrayBuffer)), { once: true }) + ); + socket.send(encodeDeviceFrame({ s: "hs2", k: "chan", to: "peer" }, new Uint8Array([1, 2, 3]))); + const bounce = decodeDeviceFrame(await relayed); + expect(bounce.header.k).toBe(" relay"); + expect(JSON.parse(new TextDecoder().decode(bounce.payload))).toEqual({ error: "client_gone" }); + const closed = new Promise((resolve) => socket.addEventListener("close", (event) => resolve(event.code), { once: true })); + socket.send(encodeDeviceFrame({ s: "rpc", k: "rpc", to: "peer" }, new TextEncoder().encode("plaintext canary"))); + expect(await closed).toBe(4403); + }); + + it("serializes competing genesis publications without a storage exception", async () => { + const { headers, vault } = profile(); + const publish = () => vault.fetch("https://vault/membership", { + method: "POST", headers, body: decodeBase64(fixture.membership[0]!)! + }); + const results = await Promise.all([publish(), publish()]); + expect(results.map((r) => r.status).sort()).toEqual([200, 409]); + const page = await vault.fetch("https://vault/membership?after=-1", { headers }); + expect((await page.json<{ records: string[] }>()).records).toHaveLength(1); + }); +}); diff --git a/edge/vitest.workerd.config.ts b/edge/vitest.workerd.config.ts index 2468ef2c9..4bd73be4c 100644 --- a/edge/vitest.workerd.config.ts +++ b/edge/vitest.workerd.config.ts @@ -18,8 +18,13 @@ export default defineConfig({ main: "./test/workerd/fixture.ts", miniflare: { compatibilityDate: "2026-07-01", + bindings: { AUTH_MODE: "workos" }, durableObjects: { - TEST_LOG: { className: "TestLogRoom", useSQLite: true } + TEST_LOG: { className: "TestLogRoom", useSQLite: true }, + TEST_CHAT: { className: "ChatRoom", useSQLite: true }, + TEST_REGISTRY: { className: "RegistryRoom", useSQLite: true }, + TEST_DEVICE: { className: "DeviceRoom", useSQLite: true }, + VAULT_ROOMS: { className: "VaultRoom", useSQLite: true } } } }) diff --git a/edge/wrangler.chat2test.jsonc b/edge/wrangler.chat2test.jsonc index 3f74c50a2..c3bb7c6a0 100644 --- a/edge/wrangler.chat2test.jsonc +++ b/edge/wrangler.chat2test.jsonc @@ -17,13 +17,15 @@ { "name": "SESSION_ROOMS", "class_name": "SessionRoom" }, { "name": "DEVICE_ROOMS", "class_name": "DeviceRoom" }, { "name": "REGISTRY_ROOMS", "class_name": "RegistryRoom" }, - { "name": "CHAT_ROOMS", "class_name": "ChatRoom" } + { "name": "CHAT_ROOMS", "class_name": "ChatRoom" }, + { "name": "VAULT_ROOMS", "class_name": "VaultRoom" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["SessionRoom", "DeviceRoom"] }, { "tag": "v2", "new_sqlite_classes": ["RegistryRoom"] }, - { "tag": "v3", "new_sqlite_classes": ["ChatRoom"] } + { "tag": "v3", "new_sqlite_classes": ["ChatRoom"] }, + { "tag": "v4", "new_sqlite_classes": ["VaultRoom"] } ], "r2_buckets": [ { "binding": "BLOBS", "bucket_name": "comet-native-blobs-chat2test" }, diff --git a/edge/wrangler.jsonc b/edge/wrangler.jsonc index 46c66f3b3..7ba180e4b 100644 --- a/edge/wrangler.jsonc +++ b/edge/wrangler.jsonc @@ -40,7 +40,8 @@ { "name": "SESSION_ROOMS", "class_name": "SessionRoom" }, { "name": "DEVICE_ROOMS", "class_name": "DeviceRoom" }, { "name": "REGISTRY_ROOMS", "class_name": "RegistryRoom" }, - { "name": "CHAT_ROOMS", "class_name": "ChatRoom" } + { "name": "CHAT_ROOMS", "class_name": "ChatRoom" }, + { "name": "VAULT_ROOMS", "class_name": "VaultRoom" } ] }, "migrations": [ @@ -59,6 +60,12 @@ // replace SessionRoom's loro-aware s2 rooms. "tag": "v3", "new_sqlite_classes": ["ChatRoom"] + }, + { + // Encrypted-sync control plane (docs/rfc-0001-encrypted-sync.txt §6): + // per-profile membership history, key envelopes, enrollment. + "tag": "v4", + "new_sqlite_classes": ["VaultRoom"] } ], "r2_buckets": [ diff --git a/scripts/check-crypto-capabilities.swift b/scripts/check-crypto-capabilities.swift new file mode 100644 index 000000000..06be66cd5 --- /dev/null +++ b/scripts/check-crypto-capabilities.swift @@ -0,0 +1,451 @@ +import CryptoKit +import Foundation + +struct ProbeFailure: Error { + let check: String +} + +func check(_ name: String, _ condition: Bool) throws { + guard condition else { throw ProbeFailure(check: name) } + print("PASS: \(name)") +} + +func rejects(_ name: String, _ expected: VaultCryptoError, _ operation: () throws -> Void) throws { + do { + try operation() + } catch let error as VaultCryptoError where error == expected { + return + } + throw ProbeFailure(check: name) +} + +func hex(_ value: String) throws -> Data { + let bytes = Array(value.utf8) + guard bytes.count.isMultiple(of: 2) else { throw ProbeFailure(check: "odd hex length") } + return try Data(stride(from: 0, to: bytes.count, by: 2).map { i in + guard let byte = UInt8(String(decoding: bytes[i...i + 1], as: UTF8.self), radix: 16) else { + throw ProbeFailure(check: "invalid fixture hex") + } + return byte + }) +} + +struct PrimitiveVectors: Codable { + let version: Int + let aes256gcm: [AESVector] + var ed25519: [SignatureVector] + let hkdfSha256: [HKDFVector] + var signedRecords: [SignedRecordVector] + let recordMutations: [RecordMutation] + let ed25519PointEncodings: [EncodingVector] + let ed25519ScalarEncodings: [EncodingVector] + let ed25519Rejections: [RejectionVector] + var encryptedContent: [EncryptedContentVector] +} + +struct EncryptedContentVector: Codable { + let name, vaultId, generation, objectId, authorId, membershipHash: String + let epoch, purpose: UInt64 + let keyId, contentKey, signerSeed, publicKey, plaintext, encoded: String + var peerRecord: String? +} + +struct EncodingVector: Codable { + let name, encoding: String + let allowed: Bool +} + +struct RejectionVector: Codable { + let name, publicKey, message, signature: String +} + +struct AESVector: Codable { + let name, key, nonce, aad, plaintext, ciphertext, tag: String +} + +struct SignatureVector: Codable { + let name, seed, publicKey, message, signature: String + var peerSignature: String? +} + +struct HKDFVector: Codable { + let name, ikm, salt, info, output: String +} + +struct SignedRecordVector: Codable { + let name: String + let kind, epoch: UInt64 + let vaultId, generation, objectId, authorId, revisionId, membershipHash: String + let payload, seed, publicKey, signingBytes, signature: String + var peerSignature: String? + var peerRecord: String? +} + +struct RecordMutation: Codable { + let name: String + let offset, remove: Int + let insert, error: String +} + +@main +struct CryptoCapabilityProbe { + static func main() throws { + let path = CommandLine.arguments.dropFirst().first ?? "crates/crypto/tests/fixtures/primitives.json" + var vectors = try JSONDecoder().decode(PrimitiveVectors.self, from: Data(contentsOf: URL(fileURLWithPath: path))) + try check("shared fixture version and nonempty groups", vectors.version == 1 && !vectors.aes256gcm.isEmpty && !vectors.ed25519.isEmpty && !vectors.hkdfSha256.isEmpty) + for vector in vectors.aes256gcm { try aes(vector) } + for index in vectors.ed25519.indices { + vectors.ed25519[index].peerSignature = try signature(vectors.ed25519[index]) + } + for vector in vectors.hkdfSha256 { try hkdf(vector) } + try ed25519EncodingChecks(vectors) + try check("signed-wrapper fixture groups", !vectors.signedRecords.isEmpty && vectors.recordMutations.count >= 21) + for index in vectors.signedRecords.indices { + let (signature, record) = try signedRecord(vectors.signedRecords[index], mutations: vectors.recordMutations) + vectors.signedRecords[index].peerSignature = signature.map { String(format: "%02x", $0) }.joined() + vectors.signedRecords[index].peerRecord = record.map { String(format: "%02x", $0) }.joined() + } + try check("encrypted-content fixtures", !vectors.encryptedContent.isEmpty) + for index in vectors.encryptedContent.indices { + let record = try encryptedContent(vectors.encryptedContent[index]) + vectors.encryptedContent[index].peerRecord = record.map { String(format: "%02x", $0) }.joined() + } + try hpkeCapability() + if let outputPath = CommandLine.arguments.dropFirst(2).first { + try JSONEncoder().encode(vectors).write(to: URL(fileURLWithPath: outputPath), options: .atomic) + print("PASS: emitted public-key test vectors for Rust verification") + } + print("Core encryption conformance passed; complete sync integration and security review remain separate.") + } + + static func aes(_ v: AESVector) throws { + let key = SymmetricKey(data: try hex(v.key)) + let nonce = try hex(v.nonce) + let aad = try hex(v.aad) + let plaintext = try hex(v.plaintext) + let ciphertext = try hex(v.ciphertext + v.tag) + let opened = try VaultCrypto.openAES256GCM(key: key, nonce: nonce, aad: aad, ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count) + try check("\(v.name): open shared bytes", opened == plaintext) + let sealed = try AES.GCM.seal(plaintext, using: key, nonce: AES.GCM.Nonce(data: nonce), authenticating: aad) + try check("\(v.name): Swift seals identical shared bytes", sealed.ciphertext + sealed.tag == ciphertext) + for index in ciphertext.indices { + var damaged = ciphertext + damaged[index] ^= 1 + let before = damaged + try rejects("changed ciphertext/tag", .authenticationFailed) { + _ = try VaultCrypto.openAES256GCM(key: key, nonce: nonce, aad: aad, ciphertextAndTag: damaged, maxPlaintextBytes: plaintext.count) + } + guard damaged == before else { throw ProbeFailure(check: "input mutated") } + } + try rejects("changed AAD", .authenticationFailed) { + _ = try VaultCrypto.openAES256GCM(key: key, nonce: nonce, aad: aad + Data([0]), ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count) + } + try rejects("wrong key", .authenticationFailed) { + _ = try VaultCrypto.openAES256GCM(key: SymmetricKey(data: Data(repeating: 1, count: 32)), nonce: nonce, aad: aad, ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count) + } + try rejects("wrong nonce", .authenticationFailed) { + _ = try VaultCrypto.openAES256GCM(key: key, nonce: Data(repeating: 1, count: 12), aad: aad, ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count) + } + try rejects("plaintext budget", .sizeLimitExceeded) { + _ = try VaultCrypto.openAES256GCM(key: key, nonce: nonce, aad: aad, ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count - 1) + } + for length in [0, 16, 24, 31, 33] { + try rejects("key length", .invalidKeyLength) { + _ = try VaultCrypto.openAES256GCM(key: SymmetricKey(data: Data(repeating: 0, count: length)), nonce: nonce, aad: aad, ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count) + } + } + for length in [0, 8, 11, 13, 16] { + try rejects("nonce length", .invalidNonceLength) { + _ = try VaultCrypto.openAES256GCM(key: key, nonce: Data(repeating: 0, count: length), aad: aad, ciphertextAndTag: ciphertext, maxPlaintextBytes: plaintext.count) + } + } + for length in 0..<16 { + try rejects("ciphertext length", .invalidCiphertextLength) { + _ = try VaultCrypto.openAES256GCM(key: key, nonce: nonce, aad: aad, ciphertextAndTag: ciphertext.prefix(length), maxPlaintextBytes: plaintext.count) + } + } + let sliced = (Data([255]) + ciphertext).dropFirst() + try check("\(v.name): sliced Data and negative cases", VaultCrypto.openAES256GCM(key: key, nonce: nonce, aad: aad, ciphertextAndTag: sliced, maxPlaintextBytes: plaintext.count) == plaintext) + } + + static func signature(_ v: SignatureVector) throws -> String { + let key = try hex(v.publicKey) + let message = try hex(v.message) + let sig = try hex(v.signature) + try VaultCrypto.verifyEd25519(publicKey: key, message: message, signature: sig) + let signer = try Curve25519.Signing.PrivateKey(rawRepresentation: hex(v.seed)) + let generated = try signer.signature(for: message) + try check("\(v.name): public key matches shared bytes", signer.publicKey.rawRepresentation == key) + try VaultCrypto.verifyEd25519(publicKey: key, message: message, signature: generated) + try rejects("changed message", .authenticationFailed) { + try VaultCrypto.verifyEd25519(publicKey: key, message: message + Data([0]), signature: sig) + } + for index in sig.indices { + var damaged = sig + damaged[index] ^= 1 + try rejects("changed signature", .authenticationFailed) { + try VaultCrypto.verifyEd25519(publicKey: key, message: message, signature: damaged) + } + } + try rejects("wrong signer", .authenticationFailed) { + try VaultCrypto.verifyEd25519(publicKey: Data(repeating: 0, count: 32), message: message, signature: sig) + } + for length in [0, 31, 33] { + try rejects("public key length", .invalidKeyLength) { + try VaultCrypto.verifyEd25519(publicKey: Data(repeating: 0, count: length), message: message, signature: sig) + } + } + for length in [0, 63, 65] { + try rejects("signature length", .invalidSignatureLength) { + try VaultCrypto.verifyEd25519(publicKey: key, message: message, signature: Data(repeating: 0, count: length)) + } + } + print("PASS: \(v.name): verification and negative cases") + return generated.map { String(format: "%02x", $0) }.joined() + } + + static func hkdf(_ v: HKDFVector) throws { + let ikm = SymmetricKey(data: try hex(v.ikm)) + let salt = try hex(v.salt) + let info = try hex(v.info) + let output = try hex(v.output) + let derived = try VaultCrypto.hkdfSHA256(inputKeyMaterial: ikm, salt: salt, info: info, outputByteCount: output.count) + try check("\(v.name): derive shared bytes", derived.withUnsafeBytes { Data($0) } == output) + let separated = try VaultCrypto.hkdfSHA256(inputKeyMaterial: ikm, salt: salt, info: info + Data([0]), outputByteCount: output.count) + try check("\(v.name): info-label separation", separated != derived) + for length in [-1, 0, 8161, Int.max] { + try rejects("HKDF output bounds", .invalidOutputLength) { + _ = try VaultCrypto.hkdfSHA256(inputKeyMaterial: ikm, salt: salt, info: info, outputByteCount: length) + } + } + for length in [1, 32, 8160] { + let result = try VaultCrypto.hkdfSHA256(inputKeyMaterial: ikm, salt: salt, info: info, outputByteCount: length) + guard result.bitCount == length * 8 else { throw ProbeFailure(check: "HKDF output size") } + } + print("PASS: \(v.name): output bounds") + } + + static func ed25519EncodingChecks(_ fixtures: PrimitiveVectors) throws { + guard fixtures.ed25519PointEncodings.count >= 13, fixtures.ed25519ScalarEncodings.count >= 9, + fixtures.ed25519Rejections.count >= 8 else { throw ProbeFailure(check: "Ed25519 rejection fixtures missing") } + for v in fixtures.ed25519PointEncodings { + var encoded = (Data([255]) + (try hex(v.encoding))).dropFirst() + let before = encoded + guard VaultCrypto.passesEd25519PointEncodingPrecheck(encoded) == v.allowed, encoded == before else { + throw ProbeFailure(check: v.name) + } + if encoded.count == 32 { + encoded[encoded.index(before: encoded.endIndex)] ^= 0x80 + guard VaultCrypto.passesEd25519PointEncodingPrecheck(encoded) == v.allowed else { + throw ProbeFailure(check: "\(v.name): opposite sign") + } + } + } + for v in fixtures.ed25519ScalarEncodings { + let encoded = (Data([255]) + (try hex(v.encoding))).dropFirst() + guard VaultCrypto.passesEd25519ScalarEncodingPrecheck(encoded) == v.allowed else { + throw ProbeFailure(check: v.name) + } + } + for lowByte: UInt8 in 0xed...0xff { + for highByte: UInt8 in [0x7f, 0xff] { + var encoded = Data(repeating: 0xff, count: 32) + encoded[0] = lowByte + encoded[31] = highByte + guard !VaultCrypto.passesEd25519PointEncodingPrecheck(encoded) else { + throw ProbeFailure(check: "noncanonical field coordinate accepted") + } + } + } + for v in fixtures.ed25519Rejections { + let key = try hex(v.publicKey) + let message = try hex(v.message) + let signature = try hex(v.signature) + guard key.count == 32, signature.count == 64 else { throw ProbeFailure(check: "\(v.name): invalid fixture length") } + try rejects(v.name, .authenticationFailed) { + try VaultCrypto.verifyEd25519(publicKey: key, message: message, signature: signature) + } + } + print("PASS: shared Ed25519 encoding prechecks, sign variants, field/scalar bounds, and rejection vectors") + } + + static func rejectsRecord(_ name: String, _ expected: VaultRecordError, _ operation: () throws -> Void) throws { + do { + try operation() + } catch let error as VaultRecordError where error == expected { + return + } + throw ProbeFailure(check: name) + } + + static func signedRecord(_ v: SignedRecordVector, mutations: [RecordMutation]) throws -> (Data, Data) { + guard let kind = VaultRecordKind(rawValue: v.kind) else { throw ProbeFailure(check: "fixture record kind") } + let binding = try VaultRecordBinding( + kind: kind, vaultId: hex(v.vaultId), generation: hex(v.generation), epoch: v.epoch, + objectId: hex(v.objectId), authorId: hex(v.authorId), membershipHash: hex(v.membershipHash) + ) + let revision = try hex(v.revisionId) + let payload = try hex(v.payload) + let signature = try hex(v.signature) + let publicKey = try hex(v.publicKey) + let input = try VaultRecordCodec.signingBytes(binding: binding, revisionId: revision, payload: payload, maxPayloadBytes: payload.count) + try check("\(v.name): canonical signed bytes", input == hex(v.signingBytes)) + let encoded = try VaultRecordCodec.encodeSigned(binding: binding, revisionId: revision, payload: payload, signature: signature, maxPayloadBytes: payload.count) + let expected = try Data([0xab]) + Data(hex(v.signingBytes).dropFirst(24)) + Data([0x0a, 0x58, 0x40]) + signature + try check("\(v.name): canonical signed map", encoded == expected) + let unverified = try VaultUnverifiedRecord.parse((Data([255]) + encoded).dropFirst(), maxPayloadBytes: payload.count) + try check("\(v.name): unverified metadata and redaction", unverified.untrustedBinding == binding && unverified.untrustedRevisionId == revision && String(reflecting: unverified) == "UnverifiedRecord([REDACTED])") + let verified = try unverified.verify(expected: binding, trustedPublicKey: publicKey) + try check("\(v.name): verify Rust fixture with sliced input", verified.payload == payload && verified.revisionId == revision && verified.binding == binding && String(reflecting: verified) == "VerifiedRecord([REDACTED])") + var wrong = binding + wrong.vaultId[wrong.vaultId.startIndex] ^= 1 + try rejectsRecord("wrong trusted binding", .contextMismatch) { + _ = try unverified.verify(expected: wrong, trustedPublicKey: publicKey) + } + for key in [Data(repeating: 0, count: 32), Data(repeating: 0, count: 31)] { + try rejectsRecord("wrong trusted key", .invalidSignature) { + _ = try unverified.verify(expected: binding, trustedPublicKey: key) + } + } + for index in encoded.indices { + var damaged = encoded + damaged[index] ^= 1 + let before = damaged + do { + _ = try VaultUnverifiedRecord.parse(damaged, maxPayloadBytes: payload.count).verify(expected: binding, trustedPublicKey: publicKey) + } catch is VaultRecordError { + guard damaged == before else { throw ProbeFailure(check: "record input mutated") } + continue + } + throw ProbeFailure(check: "record byte tampering accepted") + } + for length in 0.. Data { + guard let purpose = VaultContentPurpose(rawValue: vector.purpose) else { throw ProbeFailure(check: "fixture purpose") } + let binding = try VaultRecordBinding( + kind: .content, vaultId: hex(vector.vaultId), generation: hex(vector.generation), epoch: vector.epoch, + objectId: hex(vector.objectId), authorId: hex(vector.authorId), membershipHash: hex(vector.membershipHash) + ) + let key = try VaultContentKey(scope: VaultKeyScope(binding), identifier: hex(vector.keyId), bytes: hex(vector.contentKey)) + let signer = try VaultDeviceSigner(authorId: binding.authorId, seed: hex(vector.signerSeed)) + try check("\(vector.name): fixture signing key", signer.publicKey == hex(vector.publicKey)) + let plaintext = try hex(vector.plaintext) + let encoded = try hex(vector.encoded) + let opened = try VaultContentCrypto.open((Data([255]) + encoded).dropFirst(), expected: binding, purpose: purpose, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: plaintext.count) + try check("\(vector.name): Swift decrypts Rust ciphertext", opened.plaintext == plaintext) + let sealed = try VaultContentCrypto.seal(binding: binding, purpose: purpose, key: key, signer: signer, plaintext: plaintext, maxPlaintextBytes: plaintext.count) + let again = try VaultContentCrypto.seal(binding: binding, purpose: purpose, key: key, signer: signer, plaintext: plaintext, maxPlaintextBytes: plaintext.count) + try check("\(vector.name): fresh record identity and ciphertext", sealed.revisionId != again.revisionId && sealed.encoded != again.encoded) + try check("\(vector.name): native round trip", VaultContentCrypto.open(sealed.encoded, expected: binding, purpose: purpose, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: plaintext.count).plaintext == plaintext) + if plaintext.count >= 16 { + try check("\(vector.name): plaintext canary absent", sealed.encoded.range(of: plaintext) == nil) + } + for index in encoded.indices { + var corrupted = encoded + corrupted[index] ^= 1 + do { + _ = try VaultContentCrypto.open(corrupted, expected: binding, purpose: purpose, key: key, trustedPublicKey: signer.publicKey, maxPlaintextBytes: plaintext.count) + } catch is VaultContentError { continue } + throw ProbeFailure(check: "encrypted record tampering accepted") + } + for length in 0.. Bool) throws { + guard try condition() else { throw Failure(check: name) } + passed += 1 + print("PASS: \(name)") +} +func throws_(_ name: String, _ operation: () throws -> Void) throws { + do { try operation() } catch { passed += 1; print("PASS: \(name) (rejected: \(error))"); return } + throw Failure(check: "\(name) — accepted") +} +func hex(_ text: String) -> Data { + var out = Data(); var i = text.startIndex + while i < text.endIndex { let n = text.index(i, offsetBy: 2); out.append(UInt8(text[i.. String { data.map { String(format: "%02x", $0) }.joined() } +func b64(_ text: String) -> Data { Data(base64Encoded: text)! } + +@main +struct Probe { + static func main() throws { + let path = CommandLine.arguments.dropFirst().first ?? "crates/crypto/tests/fixtures/vault.json" + let fixture = try JSONDecoder().decode(Fixture.self, from: Data(contentsOf: URL(fileURLWithPath: path))) + let records = fixture.membership.map(b64) + let profile = VaultPolicy.profileHash(orgId: fixture.orgId, userId: fixture.userId) + try check("profile hash agrees", hex(profile) == fixture.profileHash) + var states = [try VaultMembershipState.fromGenesis(records[0], expectedVaultId: hex(fixture.vaultId), + expectedGeneration: hex(fixture.generation), expectedProfileHash: profile)] + for record in records.dropFirst() { states.append(try states.last!.apply(record)) } + try check("membership hashes agree", states.map { hex($0.hash) } == fixture.membershipHashes) + try check("epochs agree", states.map(\.epoch) == fixture.epochsAfter) + try check("device B revoked at head", states.last!.device(hex(fixture.deviceB.id))?.status == .revoked) + try check("recovery authority id agrees", hex(states.last!.recoveryAuthorityId) == fixture.recoveryAuthorityId) + try throws_("replay on moved head") { _ = try states[1].apply(records[1]) } + try throws_("fork onto genesis") { _ = try states[0].apply(records[2]) } + for index in [0, 5, 40, records[1].count - 1] { + var damaged = records[1]; damaged[damaged.startIndex + index] ^= 1 + try throws_("mutation at byte \(index)") { _ = try states[0].apply(damaged) } + } + try throws_("wrong profile at genesis") { + _ = try VaultMembershipState.fromGenesis(records[0], expectedVaultId: hex(fixture.vaultId), + expectedGeneration: hex(fixture.generation), expectedProfileHash: Data(repeating: 0, count: 32)) + } + + let added = states[1] + let aId = hex(fixture.deviceA.id), aPublic = hex(fixture.deviceA.signingKey) + let bKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: hex(fixture.deviceB.encryptionSecret)) + let keyring = try VaultEnvelope.openKeyring( + b64(fixture.keyringEnvelopeB), expected: added.envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: 1, authorId: aId), + recipientKind: .device, recipientId: hex(fixture.deviceB.id), recipientKey: bKey, trustedPublicKey: aPublic) + try check("keyring envelope opens (HPKE)", hex(keyring.epochKey(1)!) == fixture.keyringEpoch1) + try check("keyring codec round trips", try VaultKeyring.decode(keyring.encode()) == keyring) + try throws_("keyring envelope wrong recipient kind") { + _ = try VaultEnvelope.openKeyring(b64(fixture.keyringEnvelopeB), expected: added.envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: 1, authorId: aId), + recipientKind: .recovery, recipientId: hex(fixture.deviceB.id), recipientKey: bKey, trustedPublicKey: aPublic) + } + try throws_("keyring envelope stale head") { + _ = try VaultEnvelope.openKeyring(b64(fixture.keyringEnvelopeB), expected: states[2].envelopeBinding(objectId: VaultPolicy.policyObjectId, epoch: 1, authorId: aId), + recipientKind: .device, recipientId: hex(fixture.deviceB.id), recipientKey: bKey, trustedPublicKey: aPublic) + } + let objectId = hex(fixture.objectId) + let objectKey = try VaultEnvelope.unwrapObjectKey(b64(fixture.objectKeyEnvelope), + expected: added.envelopeBinding(objectId: objectId, epoch: 1, authorId: aId), epochKey: keyring.epochKey(1)!, trustedPublicKey: aPublic) + try check("object key unwraps", hex(objectKey.identifier) == fixture.objectKeyId && hex(objectKey.exposeSecret()) == fixture.objectKey) + let opened = try VaultContentCrypto.open(b64(fixture.chatRecord), expected: added.contentBinding(objectId: objectId, authorId: aId), + purpose: .chatUpdate, key: objectKey, trustedPublicKey: aPublic, maxPlaintextBytes: 1024) + try check("chat record opens", String(decoding: opened.plaintext, as: UTF8.self) == fixture.chatPlaintext) + try throws_("chat record under moved head") { + _ = try VaultContentCrypto.open(b64(fixture.chatRecord), expected: states[2].contentBinding(objectId: objectId, authorId: aId), + purpose: .chatUpdate, key: objectKey, trustedPublicKey: aPublic, maxPlaintextBytes: 1024) + } + + let secret = try VaultRecoverySecret(kit: fixture.recoveryKit) + try check("recovery kit decodes", hex(secret.secret) == fixture.recoverySecret && secret.kit == fixture.recoveryKit) + try check("recovery signing key agrees", hex(try secret.signingKey().publicKey.rawRepresentation) == fixture.recoverySigningKey) + try check("recovery encryption key agrees", hex(try secret.encryptionKey().publicKey.rawRepresentation) == fixture.recoveryEncryptionKey) + try check("recovery authority id from kit", hex(try secret.authorityId()) == fixture.recoveryAuthorityId) + try check("kit parsing is tolerant", try VaultRecoverySecret(kit: fixture.recoveryKit.lowercased().replacingOccurrences(of: "-", with: " ")).secret == secret.secret) + var damagedKit = Array(fixture.recoveryKit); damagedKit[0] = damagedKit[0] == "A" ? "B" : "A" + try throws_("kit checksum") { _ = try VaultRecoverySecret(kit: String(damagedKit)) } + + let request = VaultEnrollmentRequest(vaultId: hex(fixture.vaultId), requestId: hex(fixture.enrollment.requestId), + deviceId: hex(fixture.enrollment.deviceId), signingKey: hex(fixture.enrollment.signingKey), encryptionKey: hex(fixture.enrollment.encryptionKey)) + try request.verify(proof: hex(fixture.enrollment.proof)); passed += 1; print("PASS: enrollment proof verifies") + let genesis = hex(fixture.membershipHashes[0]) + try check("pairing code agrees", request.pairingCode(genesisHash: genesis) == fixture.enrollment.pairingCode) + var swapped = request; swapped.encryptionKey = Data(repeating: 9, count: 32) + try throws_("enrollment proof with swapped key") { try swapped.verify(proof: hex(fixture.enrollment.proof)) } + try check("pairing code differs for another vault", request.pairingCode(genesisHash: Data(repeating: 0, count: 32)) != fixture.enrollment.pairingCode) + print("\(passed) checks passed") + } +} diff --git a/scripts/test-ios-vault-live.sh b/scripts/test-ios-vault-live.sh new file mode 100755 index 000000000..b259e2bc2 --- /dev/null +++ b/scripts/test-ios-vault-live.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Actual iOS clients <-> local workerd <-> Rust vault/HostRelay. +set -euo pipefail +cd "$(dirname "$0")/.." +root="$PWD" +run_dir=/tmp/comet-mobile-e2e +mkdir -p "$run_dir" +if ! mkdir "$run_dir/lock" 2>/dev/null; then + echo "Another mobile vault test owns $run_dir/lock" >&2 + exit 1 +fi +host_pid= +edge_pid= +cleanup() { + if [[ -n "$host_pid" ]]; then kill "$host_pid" 2>/dev/null || true; fi + if [[ -n "$edge_pid" ]]; then kill "$edge_pid" 2>/dev/null || true; fi + rm -f "$run_dir/connection.json" "$run_dir/revoke" "$run_dir/revoked" "$run_dir/done" + rmdir "$run_dir/lock" +} +trap cleanup EXIT +rm -f "$run_dir/connection.json" "$run_dir/revoke" "$run_dir/revoked" "$run_dir/done" +port="${ZERON_MOBILE_TEST_PORT:-27641}" +edge_url="http://127.0.0.1:$port" +(cd edge && exec ./node_modules/.bin/wrangler dev --local --ip 127.0.0.1 --port "$port" \ + --inspector-port 0 --var AUTH_MODE:dev --persist-to "$run_dir/edge") > "$run_dir/edge.log" 2>&1 & +edge_pid=$! +for _ in {1..100}; do + if curl -s -o /dev/null "$edge_url"; then break; fi + if ! kill -0 "$edge_pid" 2>/dev/null; then cat "$run_dir/edge.log"; exit 1; fi + sleep 0.2 +done +ZERON_VAULT_EDGE_URL="$edge_url" ZERON_MOBILE_E2E_DIR="$run_dir" \ + cargo test -p zeron-engine --test vault_e2e mobile_test_host -- --nocapture > "$run_dir/host.log" 2>&1 & +host_pid=$! +for _ in {1..600}; do + if [[ -f "$run_dir/connection.json" ]]; then break; fi + if ! kill -0 "$host_pid" 2>/dev/null; then cat "$run_dir/host.log"; exit 1; fi + sleep 0.2 +done +[[ -f "$run_dir/connection.json" ]] || { echo "Host did not start; see $run_dir/host.log" >&2; exit 1; } +xcodebuild -project "$root/apps/ios/Zeron.xcodeproj" -scheme Zeron \ + -destination "${ZERON_IOS_TEST_DESTINATION:-platform=iOS Simulator,name=iPhone 17 Pro}" \ + -derivedDataPath "${ZERON_IOS_TEST_BUILD_DIR:-/tmp/comet-ios-e2ee}" \ + -only-testing:ZeronTests/MobileVaultLiveTests \ + -only-testing:ZeronTests/VaultChannelTests \ + -only-testing:ZeronTests/SessionSidecarsTests test > "$run_dir/ios.log" 2>&1 || { + tail -80 "$run_dir/ios.log" + exit 1 + } +wait "$host_pid" +host_pid= +echo "iOS enrollment, encrypted RPC, sidecars, reconnect and revocation passed. Logs: $run_dir"