From 4fa6cc7d79e4a8f342f3322d14062c6c1861e586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Gro=C3=9Fmann?= Date: Sat, 28 Jan 2023 02:50:14 +0100 Subject: [PATCH] WIP: rekey module to rekey all secrets using the yubikey Work apart from interactivity. Pins are thus currently unsopported Will be supperseeded by a flake runable to rekey secrets on demand --- README.md | 5 + configuration.nix | 68 ++++++----- data/gpg/gpg.conf.nix | 120 +++++++++---------- flake.lock | 22 ++-- flake.nix | 57 +++++---- keys/patricknix.pub | 1 + modules/pipewire.nix | 6 +- modules/rekey.nix | 134 +++++++++++++++++++++ secrets/iwd/devolo-og.psk.age | Bin 828 -> 951 bytes secrets/iwd/eduroam.8021x.age | Bin 505 -> 691 bytes secrets/recipients.txt | 6 + users/common/autorandr.nix | 215 +++++++++++++++++----------------- users/common/default.nix | 109 +++++++++-------- users/common/desktop.nix | 10 +- users/common/zsh.nix | 25 ++-- users/default.nix | 3 +- users/patrick.nix | 47 ++++---- 17 files changed, 501 insertions(+), 327 deletions(-) create mode 100644 README.md create mode 100644 keys/patricknix.pub create mode 100644 modules/rekey.nix create mode 100644 secrets/recipients.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf61f6b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Meine wundervolle nix config + +For secrets: + - encrypt using: `rage -R recipients.txt -o [OUT] -e [IN] ` + - decrypt using: `rage -R recipients.txt -o [OUT] -d [IN] ` diff --git a/configuration.nix b/configuration.nix index 576d4f1..e64567f 100644 --- a/configuration.nix +++ b/configuration.nix @@ -4,7 +4,7 @@ { config, pkgs, - age, + lib, ... }: { imports = [ @@ -12,8 +12,9 @@ ./hardware-configuration.nix #user home configuration ./users - # - ./modules/pipewire.nix + # + ./modules/pipewire.nix + ./modules/rekey.nix ]; # Use the systemd-boot EFI boot loader. @@ -22,19 +23,23 @@ networking.hostName = "patricknix"; # Define your hostname. networking.hostId = "68438432"; - # Pick only one of the below networking options. - networking.wireless.iwd.enable = true; - age.identityPaths = [ ./secrets/NIXOSc.key ./secrets/NIXOSa.key ]; - age.plugins = [ pkgs.age-plugin-yubikey ]; - age.secrets.eduroam = { - file = ./secrets/iwd/eduroam.8021x.age; - path = "/etc/iwd/eduroam.8021x"; - }; - age.secrets.devoloog = { - file = ./secrets/iwd/devolo-og.psk.age; - path = "/etc/iwd/devolo-og.psk"; - }; + # Identities with which all secrets are encrypted + rekey.masterIdentityPaths = [./secrets/NIXOSc.key ./secrets/NIXOSa.key]; + + rekey.pubKey = ./keys + "/${config.networking.hostName}.pub"; + rekey.privKey = "/etc/ssh/ssh_host_ed25519_key"; + rekey.plugins = [pkgs.age-plugin-yubikey]; + + networking.wireless.iwd.enable = true; + rekey.secrets.eduroam = { + file = ./secrets/iwd/eduroam.8021x.age; + path = "/etc/iwd/eduroam.8021x"; + }; + rekey.secrets.devoloog = { + file = ./secrets/iwd/devolo-og.psk.age; + path = "/etc/iwd/devolo-og.psk"; + }; networking.useNetworkd = true; networking.dhcpcd.enable = false; @@ -66,17 +71,17 @@ displayManager.startx.enable = true; layout = "de"; xkbVariant = "bone"; - autoRepeatDelay = 235; - autoRepeatInterval = 60; + autoRepeatDelay = 235; + autoRepeatInterval = 60; videoDrivers = ["modesetting" "nvidia"]; - libinput = { - enable = true; - mouse.accelProfile = "flat"; - touchpad = { - accelProfile = "flat"; - naturalScrolling = true; - }; - }; + libinput = { + enable = true; + mouse.accelProfile = "flat"; + touchpad = { + accelProfile = "flat"; + naturalScrolling = true; + }; + }; }; services.autorandr.enable = true; @@ -122,9 +127,9 @@ xterm wget gcc - tree - age-plugin-yubikey - rage + tree + age-plugin-yubikey + rage ]; # List services that you want to enable: @@ -139,6 +144,9 @@ }; hostKeys = [ { + # never set this to an actual nix type path + # or else ..... + # it will end up in the nix store path = "/etc/ssh/ssh_host_ed25519_key"; type = "ed25519"; } @@ -196,6 +204,10 @@ ]; cores = 0; max-jobs = "auto"; + + # If the yubikey is needed for rekeying my secrets the sandbox need acces to the pcscd daemon socket + # TODO only give the one derivation access to this path + extra-sandbox-paths = lib.mkIf (lib.elem pkgs.age-plugin-yubikey config.rekey.plugins) ["/run/pcscd/"]; }; daemonCPUSchedPolicy = "batch"; daemonIOSchedPriority = 5; diff --git a/data/gpg/gpg.conf.nix b/data/gpg/gpg.conf.nix index 3d992f8..c3c42bc 100644 --- a/data/gpg/gpg.conf.nix +++ b/data/gpg/gpg.conf.nix @@ -1,62 +1,62 @@ { -# https://github.com/drduh/config/blob/master/gpg.conf -# https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html -# https://www.gnupg.org/documentation/manuals/gnupg/GPG-Esoteric-Options.html -# Use AES256, 192, or 128 as cipher -"personal-cipher-preferences" = "AES256 AES192 AES"; -# Use SHA512, 384, or 256 as digest -"personal-digest-preferences" = "SHA512 SHA384 SHA256"; -# Use ZLIB, BZIP2, ZIP, or no compression -"personal-compress-preferences" = "ZLIB BZIP2 ZIP Uncompressed"; -# Default preferences for new keys -"default-preference-list" = "SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed"; -# SHA512 as digest to sign keys -"cert-digest-algo" = "SHA512"; -# SHA512 as digest for symmetric ops -"s2k-digest-algo" = "SHA512"; -# AES256 as cipher for symmetric ops -"s2k-cipher-algo" = "AES256"; -# UTF-8 support for compatibility -"charset" = "utf-8"; -# Show Unix timestamps -"fixed-list-mode" = true; -# No comments in signature -"no-comments" = true; -# No version in signature -"no-emit-version" = true; -# Disable banner -"no-greeting" = true; -# Long hexidecimal key format -"keyid-format 0xlong" = true; -# Display UID validity -"list-options" = "show-uid-validity"; -"verify-options" = "show-uid-validity"; -# Display all keys and their fingerprints -"with-fingerprint" = true; -# Display key origins and updates -#with-key-origin -# Cross-certify subkeys are present and valid -"require-cross-certification" = true; -# Disable caching of passphrase for symmetrical ops -"no-symkey-cache" = true; -# Enable smartcard -"use-agent" = true; -# Disable recipient key ID in messages -"throw-keyids" = true; -# Default/trusted key ID to use (helpful with throw-keyids) -#default-key 0xFF3E7D88647EBCDB -#trusted-key 0xFF3E7D88647EBCDB -# Group recipient keys (preferred ID last) -#group keygroup = 0xFF00000000000001 0xFF00000000000002 0xFF3E7D88647EBCDB -# Keyserver URL -#keyserver hkps://keys.openpgp.org -#keyserver hkps://keyserver.ubuntu.com:443 -#keyserver hkps://hkps.pool.sks-keyservers.net -#keyserver hkps://pgp.ocf.berkeley.edu -# Proxy to use for keyservers -#keyserver-options http-proxy=socks5-hostname://127.0.0.1:9050 -# Verbose output -#verbose -# Show expired subkeys -#list-options show-unusable-subkeys + # https://github.com/drduh/config/blob/master/gpg.conf + # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html + # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Esoteric-Options.html + # Use AES256, 192, or 128 as cipher + "personal-cipher-preferences" = "AES256 AES192 AES"; + # Use SHA512, 384, or 256 as digest + "personal-digest-preferences" = "SHA512 SHA384 SHA256"; + # Use ZLIB, BZIP2, ZIP, or no compression + "personal-compress-preferences" = "ZLIB BZIP2 ZIP Uncompressed"; + # Default preferences for new keys + "default-preference-list" = "SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed"; + # SHA512 as digest to sign keys + "cert-digest-algo" = "SHA512"; + # SHA512 as digest for symmetric ops + "s2k-digest-algo" = "SHA512"; + # AES256 as cipher for symmetric ops + "s2k-cipher-algo" = "AES256"; + # UTF-8 support for compatibility + "charset" = "utf-8"; + # Show Unix timestamps + "fixed-list-mode" = true; + # No comments in signature + "no-comments" = true; + # No version in signature + "no-emit-version" = true; + # Disable banner + "no-greeting" = true; + # Long hexidecimal key format + "keyid-format 0xlong" = true; + # Display UID validity + "list-options" = "show-uid-validity"; + "verify-options" = "show-uid-validity"; + # Display all keys and their fingerprints + "with-fingerprint" = true; + # Display key origins and updates + #with-key-origin + # Cross-certify subkeys are present and valid + "require-cross-certification" = true; + # Disable caching of passphrase for symmetrical ops + "no-symkey-cache" = true; + # Enable smartcard + "use-agent" = true; + # Disable recipient key ID in messages + "throw-keyids" = true; + # Default/trusted key ID to use (helpful with throw-keyids) + #default-key 0xFF3E7D88647EBCDB + #trusted-key 0xFF3E7D88647EBCDB + # Group recipient keys (preferred ID last) + #group keygroup = 0xFF00000000000001 0xFF00000000000002 0xFF3E7D88647EBCDB + # Keyserver URL + #keyserver hkps://keys.openpgp.org + #keyserver hkps://keyserver.ubuntu.com:443 + #keyserver hkps://hkps.pool.sks-keyservers.net + #keyserver hkps://pgp.ocf.berkeley.edu + # Proxy to use for keyservers + #keyserver-options http-proxy=socks5-hostname://127.0.0.1:9050 + # Verbose output + #verbose + # Show expired subkeys + #list-options show-unusable-subkeys } diff --git a/flake.lock b/flake.lock index 8ad81a9..e4551fa 100644 --- a/flake.lock +++ b/flake.lock @@ -7,15 +7,15 @@ ] }, "locked": { - "lastModified": 1674681075, - "narHash": "sha256-hXbIv9WHHEQvoXtK4hWKx4EzmTLUzMdjV8e/x/R9nP8=", - "owner": "oddlama", + "lastModified": 1673301561, + "narHash": "sha256-gRUWHbBAtMuPDJQXotoI8u6+3DGBIUZHkyQWpIv7WpM=", + "owner": "ryantm", "repo": "agenix", - "rev": "12d1b138188dda50704c2816be73d6e183f45797", + "rev": "42d371d861a227149dc9a7e03350c9ab8b8ddd68", "type": "github" }, "original": { - "owner": "oddlama", + "owner": "ryantm", "repo": "agenix", "type": "github" } @@ -28,11 +28,11 @@ "utils": "utils" }, "locked": { - "lastModified": 1674556204, - "narHash": "sha256-HCRmkZsq01h2Evch08zpgE9jeHdMtGdT1okWotyvuhY=", + "lastModified": 1674771519, + "narHash": "sha256-U0W3S1nX6yEvLh3Vq70EORbmXecAKXfmEfCfaA4A+I8=", "owner": "nix-community", "repo": "home-manager", - "rev": "c59f0eac51da91c6989fd13a68e156f63c0e60b6", + "rev": "bb4b25b302dbf0f527f190461b080b5262871756", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1674459583, - "narHash": "sha256-L0UZl/u2H3HGsrhN+by42c5kNYeKtdmJiPzIRvEVeiM=", + "lastModified": 1674641431, + "narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1b1f50645af2a70dc93eae18bfd88d330bfbcf7f", + "rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 496cb30..89f3777 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,44 @@ { - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - inputs.home-manager = { - url = "github:nix-community/home-manager"; - # should use system nixpkgs instead of their own - inputs.nixpkgs.follows = "nixpkgs"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { + url = "github:nix-community/home-manager"; + # should use system nixpkgs instead of their own + inputs.nixpkgs.follows = "nixpkgs"; + }; + agenix = { + url = "github:ryantm/agenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - inputs.agenix.url = "github:oddlama/agenix"; - inputs.agenix.inputs.nixpkgs.follows = "nixpkgs"; - outputs = { self, nixpkgs, home-manager, agenix, ... }: let - system = "x86_64-linux"; - in {nixosConfigurations.patricknix = - nixpkgs.lib.nixosSystem { - inherit system; + outputs = { + self, + nixpkgs, + home-manager, + agenix, + ... + }: let + system = "x86_64-linux"; + in { + nixosConfigurations.patricknix = nixpkgs.lib.nixosSystem { + inherit system; modules = [ - ./configuration.nix - home-manager.nixosModules.home-manager - { - home-manager.useGlobalPkgs = true; - home-manager.useUserPackages = true; - } - agenix.nixosModule - ]; + ./configuration.nix + home-manager.nixosModules.home-manager + { + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + } + agenix.nixosModule + { + nix.registry = { + nixpkgs.flake = nixpkgs; + p.flake = nixpkgs; + pkgs.flake = nixpkgs; + }; + } + ]; }; }; } diff --git a/keys/patricknix.pub b/keys/patricknix.pub new file mode 100644 index 0000000..c51d051 --- /dev/null +++ b/keys/patricknix.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJrr6bJgWzCuS+00EEBQRoylwput69tqvotgPjSF5xhz root@patricknix diff --git a/modules/pipewire.nix b/modules/pipewire.nix index 72e15cb..a8e6da7 100644 --- a/modules/pipewire.nix +++ b/modules/pipewire.nix @@ -8,9 +8,9 @@ hardware.pulseaudio.enable = lib.mkForce false; hardware.bluetooth.enable = true; hardware.bluetooth.settings = { - General = { - Enable = "Source,Sink,Media,Socket"; - }; + General = { + Enable = "Source,Sink,Media,Socket"; + }; }; security.rtkit.enable = true; diff --git a/modules/rekey.nix b/modules/rekey.nix new file mode 100644 index 0000000..f433267 --- /dev/null +++ b/modules/rekey.nix @@ -0,0 +1,134 @@ +{ + lib, + config, + pkgs, + stdenv, + options, + ... +}: { + # TODO add a with lib um mir die ganzen lib. zu ersparen + config = let + masterIdentities = lib.strings.concatMapStrings (x: "-i ${x} ") config.rekey.masterIdentityPaths; + rekeyedSecrets = pkgs.stdenv.mkDerivation rec { + pname = "age-rekey"; + version = "1.0.0"; + allSecrets = lib.mapAttrsToList (_: x: x.file) config.rekey.secrets; + pubKeyStr = + if builtins.isPath config.rekey.pubKey + then builtins.readFile config.rekey.pubKey + else config.rekey.pubKey; + dontMakeSourceWriteable = 1; + dontUnpack = true; + dontPatch = true; + dontConfigure = true; + dontBuild = true; + installPhase = let + pluginPaths = lib.strings.concatMapStrings (x: ":${x}/bin") config.rekey.plugins; + + rekeyCommand = secret: '' + echo "Rekeying secret ${secret}" >&2 + ${pkgs.rage}/bin/rage ${masterIdentities} -d ${secret} \ + | ${pkgs.rage}/bin/rage -r "${pubKeyStr}" -o "$out/${builtins.baseNameOf secret}" -e \ + || { echo 1 > "$out"/status; echo "disabled due to failure in rekey.nix" | ${pkgs.rage}/bin/rage -r "${pubKeyStr}" -o "$out/${builtins.baseNameOf secret}" -e ;} + ''; + in '' + set -euo pipefail + mkdir $out + echo 0 > "$out"/status + + export PATH=$PATH${pluginPaths} + ${lib.concatStringsSep "\n" (map rekeyCommand allSecrets)} + + ''; + }; + in + lib.mkIf (config.rekey.secrets != {}) { + # Polkit rule to enable the build process to access the keys saved on a yubikey + # This rule allows any user named nixbld to accesst pcscd + security.polkit.extraConfig = lib.mkIf (lib.elem pkgs.age-plugin-yubikey config.rekey.plugins) '' + polkit.addRule(function(action, subject) { + if ((action.id == "org.debian.pcsc-lite.access_pcsc" || action.id == "org.debian.pcsc-lite.access_card") && + subject.user.match(/^nixbld\d+$/)) { + return polkit.Result.YES; + } + }); + ''; + + environment.systemPackages = with pkgs; [ + rage + ]; + + age = { + secrets = let + newPath = x: "${rekeyedSecrets}/${builtins.baseNameOf x}"; + in + builtins.mapAttrs (_: + builtins.mapAttrs (name: value: + if name == "file" + then "${newPath value}" + else value)) + config.rekey.secrets; + }; + assertions = [ + { + assertion = builtins.pathExists config.rekey.pubKey; + message = "Did not find key file: ${config.rekey.pubKey}. + Make sure your public key is available for rekeying."; + } + { + assertion = config.rekey.masterIdentityPaths != []; + message = "rekey.masterIdentityPaths must be set!"; + } + ]; + warnings = + lib.optional (builtins.any (x: !(lib.strings.hasSuffix ".pub" x || lib.strings.hasSuffix ".age" x)) config.rekey.masterIdentityPaths) '' + It seems at least one of your master masterIdentities files is not encrypted or not a public handle. + Please make sure it does not contain any secret Information. + '' + ++ lib.optional (lib.toInt (builtins.readFile "${rekeyedSecrets}/status") == 1) '' + Could not rekey. Might be due to a chicken/egg problem, then a retry will fix this. + ''; + }; + + options = with lib; { + rekey.secrets = options.age.secrets; + rekey.pubKey = mkOption { + type = types.either types.path types.str; + description = '' + The age public key set as a recipient when rekeying. + either a path to a public key file or a string public key + **NEVER set this to a private key part** + ~~This will end up in the nix store.~~ + ''; + example = /etc/ssh/ssh_host_ed25519_key.pub; + }; + + rekey.privKey = mkOption { + type = types.str; + description = '' + The age private key part, corresponding to the public key set in "rekey.pubKey". + Used by agenix for decryption. + Preferably set this to your ed25519 host key. + ''; + example = "/etc/ssh/ssh_host_ed25519_key"; + }; + + rekey.masterIdentityPaths = mkOption { + type = types.listOf types.path; + description = '' + A list of Identities used for decrypting your secrets before rekeying. + **WARING this will end up in the nix-store** + Only use yubikeys or encrypted age keys + ''; + }; + + rekey.plugins = mkOption { + type = types.listOf types.package; + default = []; + description = '' + A list of plugins that should be available in your path when rekeying. + ''; + example = [pkgs.age-plugin-yubikey]; + }; + }; +} diff --git a/secrets/iwd/devolo-og.psk.age b/secrets/iwd/devolo-og.psk.age index c08e7f67922589e32f2fac94038d3817423656f3..279b538023fc81a4930ece2fdfc8356fc62b4a29 100644 GIT binary patch delta 922 zcmV;L17-ZY2Db;08Gl$ZH8n9gATvU9LUT@7L1$-EdSyp7I7CHFO-*KbFidJmZcA)v zG-yjpF?DKGZc0H(3Q}xQcvxvGY;i|TNl18hFKbaVLo#}4dNV>yGfzclPFh+{O=>oE zZ#Z&u3N1b$aA|fea56PEAXrpUYgIuYL2h+uP-H@DaWPYIWlJ<~T473MM@lzLZ8=L# zXje66Ye7wFD|Ki%NLOz)ZVGWqP)BHDb$Mk^L}OP)S9wT9 zOK~e>Gf-q$bCH1+e`ilsWi(1}X=HM0Vq#5kc`tENaa1uvMN><1b3;x>QE6p1R&rWu zQ+8Kq3TSXKPGLf9X-!IOa8ztYMrTt^WO#NkQY$iWFl9GMQ$#sWFKc&fIZSYK3N1b$ zLqJAbXhJV7XL4m>b7cxPNnvL~G+{(jc}`|^T6Sb`IeA%Se^+B>ac^i?H)S$XWoSo5 zK~-!oXhB$GXgO61EiEk|X+R@La_UWMLq|oR8#Tze8kTSyqu-M=?L?uI&b*@`BwamU^7k zQW-rqdERhLf7Xz_IrO#}RlO={a3sGrIz{8EVAo<3-new*ULH~PN)nHZ3uzgcTSbA= zvLyy3N&sS9z*W*T7TI0AQf870y%ko^0M{sZe?m0Ra%B%}2R9i=+33Gf-6>Rk;yuKc z)=Q*m+;dWZ%KCEpM=^EfD*0j=rd5{$s$d|1TT!2Oe-R$t?5n9j9|iNQ(!{Ix9s`bQ zr+Aa8L;}M!az5RrA7@Vk2+UaOx-U+706wWq0C84HkG@S0AZt@Tkd+&kV6r0?*5t&$ z@Tuq@q3pzrtxTv7<)@jNKL}UqY1jDN%iBh2FDZ+x$d!`FU_);wNlws7EkoWABd3G~P+!v)x)GAcnry%Bv ziOtIwhP5fWQg>fy!_jR1sR^tAKQEw>%Cg*ZL;;>PGqxXMTsfqAqY21Ff8kRTeXmA* wW{K>%+z8u>+&rM?48mhT)Ays$chK^JvciRkU}ffqmI9xdu1_3;DYpag8%>k delta 816 zcmV-01JC@o2fPN58GmqTb}eu+H8vnvR8ebHK_EdhVrx)QLUd zIC*GyR(V)OQ8IBvOEYbGQbAZ*ZB`0Fc4K-pW-)FvSZ85YI5SC6R#baK}v0BP-<;LdNfULGi_rEG;vg8M@KVbR7^BXZ9zssWO7GVIb~vM zY(;BFZDLY#Sx03ySZi-{FKsXiEj}P@X)R}RWnpt=AYe>Ua(5sjW-Tx>XCOmDXEBS)cG$QZY=GW1~jfe>H$&BVVKfb;U@+Vl`7 z7m4`gzHaI0Y4tmi4A{&G-<$2%ZkXA?IcRT{&rJ&%IDZAXj+t^~R$1Do!1T2k1OIXt7!swC=xTeOx(gn?(iaphyfj6 z^7qYZ?!{MOLgn9PS4f`83Ld&^{5^5StK||kRJmdA!C@Z~E>8B`bYPq~Im7 zIicu^Zi0NqjTTPu3(zrQ%LGG#ROSp+%s(rsa%oH6ZRK@lkw&sYndC}KD|yN$=gSRv zK7TxOl%X${7cjQi5AMHn+0vvK@V7?3R*@Q z4$iIOWlBf{;}%%2q|;uhjEFYppuMb`&N2iec0R#qoZs%R`5G)%P>W%ZfCxVA7HCHt zrr~%!6&84(5J2muBq3A{3FJ#Q7#Fxmq@89_xx`YsPfW)GO;=upJq8-$DUFZ?L9cLl zvF4gM-PZwKIz;AtLy-oDLsC6fjRxo@;&4l5i)0xf7-*R|lMNY~RI-5_i{o4s7ECgw zWf3$C5L&Kk;YB}YV7$o0*FpPOii{Il;gIF0a30*GaGa( zVKfO#N=LM6Dp6_EVh*8#q*_g+b7~$Yc*b-X!y;bx013q;a4!?BMc|>5 zjAdw|qXRVUZ?$4VF5?hV2=4W@{i4m7+yI^|0Kl%%QFiO-owIL#fLEsV-zgsdHIUgM zqhnd~@ZImL`i+~@QADb#e||-nN0?(qNLF!fW>RIYNnUZD zK}1++sA*uOSy7lvdSYaSzB5;9M4+!rxNAganMtZ~N^ylzxT#rWYO$eTdaz4?Ykp`! zd45HKMM+v_Svr@lodVphD7W-X&vFIF{E~V{7mKPAg9sPx-2CLkM33xZUxUiBlJt^t zM<`7sD(Ivyx;Jcc*m6!ko~={FGb|3%875vkXt; zvaFQUaD%EykYxtZ4i$O2=|!oD#iR^}&fo zDHi#Wc}eA5y1Kdw`o&f0nchJbdB&OLp4xd8DWM_xWf>L80ora6MU_qkkuHw~3??-N&3C1rIOxKYMA9^31^6_gELj~sz+2VZJy(vfardS8O z?7Vr7e@>j@ep}nhoUD`SMqP