Compare commits

..

No commits in common. "master" and "vm-test" have entirely different histories.

50 changed files with 1878 additions and 2325 deletions

6
.gitattributes vendored
View File

@ -1,3 +1,3 @@
#pattern filter=crypt diff=crypt merge=crypt #pattern filter=crypt diff=crypt
secrets/*/** filter=crypt diff=crypt merge=crypt secrets/*/** filter=crypt diff=crypt
secrets/default.nix filter=crypt diff=crypt merge=crypt secrets/default.nix filter=crypt diff=crypt

View File

@ -1,32 +0,0 @@
# Maxwell configuration
The NixOS configuration of Maxwell
## Switching configuration
1. Mount remotely the secrets directory
`$ rsshfs secrets maxwell:$PWD/secrets -o allow_other &`
2. Run nixos-rebuild
`$ nixos-rebuild test -I nixos-config=configuration.nix
--target-host maxwell --use-remote-sudo`
3. Unmount the secrets directory
`kill %%`
## Testing changes
1. Build a VM
`$ nixos-rebuild build-vm -I nixos-config=testing.nix`
2. Change the open files limit
`$ ulimit -n 90000`
3. Run the VM without internet (to avoid a mess)
`$ unshare -nc result/bin/run-maxwell-vm`

735
assets/searx-settings.yml Normal file
View File

@ -0,0 +1,735 @@
general:
debug : False # Debug mode, only for development
instance_name : "searxwell" # displayed name
search:
safe_search : 0 # Filter results. 0: None, 1: Moderate, 2: Strict
autocomplete : "" # Existing autocomplete backends: "dbpedia", "duckduckgo", "google", "startpage", "wikipedia" - leave blank to turn it off by default
language : "en-US"
ban_time_on_fail : 5 # ban time in seconds after engine errors
max_ban_time_on_fail : 120 # max ban time in seconds after engine errors
server:
port : 8083
bind_address : "127.0.0.1" # address to listen on
secret_key : "QzsHA00oA7H68Z2OXYYk3MaC6BjxkTiS"
base_url : "https://maxwell.ydns.eu/srx/"
image_proxy : False # Proxying image results through searx
http_protocol_version : "1.0" # 1.0 and 1.1 are supported
ui:
static_path : "" # Custom static path - leave it blank if you didn't change
templates_path : "" # Custom templates path - leave it blank if you didn't change
default_theme : oscar # ui theme
default_locale : "" # Default interface locale - leave blank to detect from browser information or use codes from the 'locales' config section
theme_args :
oscar_style : logicodev # default style of oscar
outgoing: # communication with search engines
request_timeout : 2.0 # seconds
useragent_suffix : "" # suffix of searx_useragent, could contain informations like an email address to the administrator
pool_connections : 100 # Number of different hosts
pool_maxsize : 10 # Number of simultaneous requests by host
engines:
- name : arch linux wiki
engine : archlinux
shortcut : al
- name : archive is
engine : xpath
search_url : https://archive.is/{query}
url_xpath : (//div[@class="TEXT-BLOCK"]/a)/@href
title_xpath : (//div[@class="TEXT-BLOCK"]/a)
content_xpath : //div[@class="TEXT-BLOCK"]/ul/li
categories : general
timeout : 7.0
disabled : True
shortcut : ai
- name : arxiv
engine : arxiv
shortcut : arx
categories : science
timeout : 4.0
- name : asksteem
engine : asksteem
shortcut : as
- name : base
engine : base
shortcut : bs
- name : wikipedia
engine : wikipedia
shortcut : wp
base_url : 'https://{language}.wikipedia.org/'
- name : bing
engine : bing
shortcut : bi
- name : bing images
engine : bing_images
shortcut : bii
- name : bing news
engine : bing_news
shortcut : bin
- name : bing videos
engine : bing_videos
shortcut : biv
- name : bitbucket
engine : xpath
paging : True
search_url : https://bitbucket.org/repo/all/{pageno}?name={query}
url_xpath : //article[@class="repo-summary"]//a[@class="repo-link"]/@href
title_xpath : //article[@class="repo-summary"]//a[@class="repo-link"]
content_xpath : //article[@class="repo-summary"]/p
categories : it
timeout : 4.0
disabled : True
shortcut : bb
- name : ccc-tv
engine : xpath
paging : False
search_url : https://media.ccc.de/search/?q={query}
url_xpath : //div[@class="caption"]/h3/a/@href
title_xpath : //div[@class="caption"]/h3/a/text()
content_xpath : //div[@class="caption"]/h4/@title
categories : videos
disabled : True
shortcut : c3tv
- name : crossref
engine : json_engine
paging : True
search_url : http://search.crossref.org/dois?q={query}&page={pageno}
url_query : doi
title_query : title
content_query : fullCitation
categories : science
shortcut : cr
- name : currency
engine : currency_convert
categories : general
shortcut : cc
- name : deezer
engine : deezer
shortcut : dz
- name : deviantart
engine : deviantart
shortcut : da
timeout: 3.0
- name : ddg definitions
engine : duckduckgo_definitions
shortcut : ddd
weight : 2
disabled : True
- name : digbt
engine : digbt
shortcut : dbt
timeout : 6.0
disabled : True
- name : digg
engine : digg
shortcut : dg
- name : erowid
engine : xpath
paging : True
first_page_num : 0
page_size : 30
search_url : https://www.erowid.org/search.php?q={query}&s={pageno}
url_xpath : //dl[@class="results-list"]/dt[@class="result-title"]/a/@href
title_xpath : //dl[@class="results-list"]/dt[@class="result-title"]/a/text()
content_xpath : //dl[@class="results-list"]/dd[@class="result-details"]
categories : general
shortcut : ew
disabled : True
- name : wikidata
engine : wikidata
shortcut : wd
timeout : 3.0
weight : 2
- name : duckduckgo
engine : duckduckgo
shortcut : ddg
disabled : True
- name : duckduckgo images
engine : duckduckgo_images
shortcut : ddi
timeout: 3.0
disabled : True
- name : etymonline
engine : xpath
paging : True
search_url : http://etymonline.com/?search={query}&p={pageno}
url_xpath : //a[contains(@class, "word--")]/@href
title_xpath : //p[contains(@class, "word__name--")]/text()
content_xpath : //section[contains(@class, "word__defination")]/object
first_page_num : 0
shortcut : et
disabled : True
- name : faroo
engine : faroo
shortcut : fa
disabled : True
- name : 1x
engine : www1x
shortcut : 1x
disabled : True
- name : fdroid
engine : fdroid
shortcut : fd
disabled : True
- name : flickr
categories : images
shortcut : fl
# You can use the engine using the official stable API, but you need an API key
# See : https://www.flickr.com/services/apps/create/
# engine : flickr
# api_key: 'apikey' # required!
# Or you can use the html non-stable engine, activated by default
engine : flickr_noapi
- name : free software directory
engine : mediawiki
shortcut : fsd
categories : it
base_url : https://directory.fsf.org/
number_of_results : 5
# what part of a page matches the query string: title, text, nearmatch
# title - query matches title, text - query matches the text of page, nearmatch - nearmatch in title
search_type : title
timeout : 5.0
disabled : True
- name : frinkiac
engine : frinkiac
shortcut : frk
disabled : True
- name : genius
engine : genius
shortcut : gen
- name : gigablast
engine : gigablast
shortcut : gb
timeout : 3.0
disabled: True
- name : gitlab
engine : json_engine
paging : True
search_url : https://gitlab.com/api/v4/projects?search={query}&page={pageno}
url_query : web_url
title_query : name_with_namespace
content_query : description
page_size : 20
categories : it
shortcut : gl
timeout : 10.0
disabled : True
- name : github
engine : github
shortcut : gh
- name : google
engine : google
shortcut : go
- name : google images
engine : google_images
shortcut : goi
- name : google news
engine : google_news
shortcut : gon
- name : google videos
engine : google_videos
shortcut : gov
- name : google scholar
engine : xpath
paging : True
search_url : https://scholar.google.com/scholar?start={pageno}&q={query}&hl=en&as_sdt=0,5&as_vis=1
results_xpath : //div[contains(@class, "gs_r")]/div[@class="gs_ri"]
url_xpath : .//h3/a/@href
title_xpath : .//h3/a
content_xpath : .//div[@class="gs_rs"]
suggestion_xpath : //div[@id="gs_qsuggest"]/ul/li
page_size : 10
first_page_num : 0
categories : science
shortcut : gos
- name : google play apps
engine : xpath
search_url : https://play.google.com/store/search?q={query}&c=apps
url_xpath : //a[@class="title"]/@href
title_xpath : //a[@class="title"]
content_xpath : //a[@class="subtitle"]
categories : files
shortcut : gpa
disabled : True
- name : google play movies
engine : xpath
search_url : https://play.google.com/store/search?q={query}&c=movies
url_xpath : //a[@class="title"]/@href
title_xpath : //a[@class="title"]/@title
content_xpath : //a[contains(@class, "subtitle")]
categories : videos
shortcut : gpm
disabled : True
- name : google play music
engine : xpath
search_url : https://play.google.com/store/search?q={query}&c=music
url_xpath : //a[@class="title"]/@href
title_xpath : //a[@class="title"]
content_xpath : //a[@class="subtitle"]
categories : music
shortcut : gps
disabled : True
- name : geektimes
engine : xpath
paging : True
search_url : https://geektimes.ru/search/page{pageno}/?q={query}
url_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]/@href
title_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]
content_xpath : //article[contains(@class, "post")]//div[contains(@class, "post__text")]
categories : it
timeout : 4.0
disabled : True
shortcut : gt
- name : habrahabr
engine : xpath
paging : True
search_url : https://habrahabr.ru/search/page{pageno}/?q={query}
url_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]/@href
title_xpath : //article[contains(@class, "post")]//a[@class="post__title_link"]
content_xpath : //article[contains(@class, "post")]//div[contains(@class, "post__text")]
categories : it
timeout : 4.0
disabled : True
shortcut : habr
- name : hoogle
engine : json_engine
paging : True
search_url : https://www.haskell.org/hoogle/?mode=json&hoogle={query}&start={pageno}
results_query : results
url_query : location
title_query : self
content_query : docs
page_size : 20
categories : it
shortcut : ho
- name : ina
engine : ina
shortcut : in
timeout : 6.0
disabled : True
- name: kickass
engine : kickass
shortcut : kc
timeout : 4.0
disabled : True
- name : library genesis
engine : xpath
search_url : https://libgen.is/search.php?req={query}
url_xpath : //a[contains(@href,"bookfi.net")]/@href
title_xpath : //a[contains(@href,"book/")]/text()[1]
content_xpath : //td/a[1][contains(@href,"=author")]/text()
categories : general
timeout : 7.0
disabled : True
shortcut : lg
- name : lobste.rs
engine : xpath
search_url : https://lobste.rs/search?utf8=%E2%9C%93&q={query}&what=stories&order=relevance
results_xpath : //li[contains(@class, "story")]
url_xpath : .//span[@class="link"]/a/@href
title_xpath : .//span[@class="link"]/a
content_xpath : .//a[@class="domain"]
categories : it
shortcut : lo
- name : microsoft academic
engine : microsoft_academic
categories : science
shortcut : ma
- name : mixcloud
engine : mixcloud
shortcut : mc
- name : nyaa
engine : nyaa
shortcut : nt
disabled : True
- name : openairedatasets
engine : json_engine
paging : True
search_url : https://api.openaire.eu/search/datasets?format=json&page={pageno}&size=10&title={query}
results_query : response/results/result
url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
title_query : metadata/oaf:entity/oaf:result/title/$
content_query : metadata/oaf:entity/oaf:result/description/$
categories : science
shortcut : oad
timeout: 5.0
- name : openairepublications
engine : json_engine
paging : True
search_url : https://api.openaire.eu/search/publications?format=json&page={pageno}&size=10&title={query}
results_query : response/results/result
url_query : metadata/oaf:entity/oaf:result/children/instance/webresource/url/$
title_query : metadata/oaf:entity/oaf:result/title/$
content_query : metadata/oaf:entity/oaf:result/description/$
categories : science
shortcut : oap
timeout: 5.0
- name : openstreetmap
engine : openstreetmap
shortcut : osm
- name : openrepos
engine : xpath
paging : True
search_url : https://openrepos.net/search/node/{query}?page={pageno}
url_xpath : //li[@class="search-result"]//h3[@class="title"]/a/@href
title_xpath : //li[@class="search-result"]//h3[@class="title"]/a
content_xpath : //li[@class="search-result"]//div[@class="search-snippet-info"]//p[@class="search-snippet"]
categories : files
timeout : 4.0
disabled : True
shortcut : or
- name : pdbe
engine : pdbe
shortcut : pdb
# Hide obsolete PDB entries.
# Default is not to hide obsolete structures
# hide_obsolete : False
- name : photon
engine : photon
shortcut : ph
- name : piratebay
engine : piratebay
shortcut : tpb
url: https://pirateproxy.red/
timeout : 3.0
- name : pubmed
engine : pubmed
shortcut : pub
categories: science
timeout : 3.0
- name : qwant
engine : qwant
shortcut : qw
categories : general
disabled : True
- name : qwant images
engine : qwant
shortcut : qwi
categories : images
- name : qwant news
engine : qwant
shortcut : qwn
categories : news
- name : qwant social
engine : qwant
shortcut : qws
categories : social media
- name : reddit
engine : reddit
shortcut : re
page_size : 25
timeout : 10.0
disabled : True
- name : scanr structures
shortcut: scs
engine : scanr_structures
disabled : True
- name : soundcloud
engine : soundcloud
shortcut : sc
- name : stackoverflow
engine : stackoverflow
shortcut : st
- name : searchcode doc
engine : searchcode_doc
shortcut : scd
- name : searchcode code
engine : searchcode_code
shortcut : scc
disabled : True
- name : framalibre
engine : framalibre
shortcut : frl
disabled : True
# - name : searx
# engine : searx_engine
# shortcut : se
# instance_urls :
# - http://127.0.0.1:8888/
# - ...
# disabled : True
- name : semantic scholar
engine : xpath
paging : True
search_url : https://www.semanticscholar.org/search?q={query}&sort=relevance&page={pageno}&ae=false
results_xpath : //article
url_xpath : .//div[@class="search-result-title"]/a/@href
title_xpath : .//div[@class="search-result-title"]/a
content_xpath : .//div[@class="search-result-abstract"]
shortcut : se
categories : science
- name : spotify
engine : spotify
shortcut : stf
- name : subtitleseeker
engine : subtitleseeker
shortcut : ss
# The language is an option. You can put any language written in english
# Examples : English, French, German, Hungarian, Chinese...
# language : English
- name : startpage
engine : startpage
shortcut : sp
timeout : 6.0
disabled : True
- name : ixquick
engine : startpage
base_url : 'https://www.ixquick.eu/'
search_url : 'https://www.ixquick.eu/do/search'
shortcut : iq
timeout : 6.0
disabled : True
- name : swisscows
engine : swisscows
shortcut : sw
disabled : True
- name : tokyotoshokan
engine : tokyotoshokan
shortcut : tt
timeout : 6.0
disabled : True
- name : torrentz
engine : torrentz
shortcut : tor
url: https://torrentz2.eu/
timeout : 3.0
- name : twitter
engine : twitter
shortcut : tw
# maybe in a fun category
# - name : uncyclopedia
# engine : mediawiki
# shortcut : unc
# base_url : https://uncyclopedia.wikia.com/
# number_of_results : 5
# tmp suspended - too slow, too many errors
# - name : urbandictionary
# engine : xpath
# search_url : http://www.urbandictionary.com/define.php?term={query}
# url_xpath : //*[@class="word"]/@href
# title_xpath : //*[@class="def-header"]
# content_xpath : //*[@class="meaning"]
# shortcut : ud
- name : yahoo
engine : yahoo
shortcut : yh
disabled : True
- name : yandex
engine : yandex
shortcut : yn
disabled : True
- name : yahoo news
engine : yahoo_news
shortcut : yhn
- name : youtube
shortcut : yt
api_key: 'AIzaSyDvEpB_xVEk3Xt0IIU8sXbyEGIdjf33CEM'
engine : youtube_api
- name : dailymotion
engine : dailymotion
shortcut : dm
- name : vimeo
engine : vimeo
shortcut : vm
- name : wolframalpha
shortcut : wa
api_key: 'AUQ8EY-H452ETQ7RL'
engine : wolframalpha_api
timeout: 6.0
categories : science
- name : seedpeer
engine : seedpeer
shortcut: speu
categories: files, music, videos
disabled: True
- name : dictzone
engine : dictzone
shortcut : dc
- name : mymemory translated
engine : translated
shortcut : tl
timeout : 5.0
disabled : True
# You can use without an API key, but you are limited to 1000 words/day
# See : http://mymemory.translated.net/doc/usagelimits.php
# api_key : ''
- name : voat
engine: xpath
shortcut: vo
categories: social media
search_url : https://searchvoat.co/?t={query}
url_xpath : //div[@class="entry"]/p/a[@class="title"]/@href
title_xpath : //div[@class="entry"]/p/a[@class="title"]
content_xpath : //div[@class="entry"]/p/span[@class="domain"]
timeout : 10.0
disabled : True
- name : 1337x
engine : 1337x
shortcut : 1337x
disabled : True
- name : seznam
shortcut: szn
engine: xpath
paging : True
search_url : https://search.seznam.cz/?q={query}&count=10&from={pageno}
results_xpath: //div[@class="Page-content"]//div[@class="Result "]
url_xpath : ./h3/a/@href
title_xpath : ./h3
content_xpath : .//p[@class="Result-description"]
first_page_num : 0
page_size : 10
disabled : True
# - name : yacy
# engine : yacy
# shortcut : ya
# base_url : 'http://localhost:8090'
# number_of_results : 5
# timeout : 3.0
# Doku engine lets you access to any Doku wiki instance:
# A public one or a privete/corporate one.
# - name : ubuntuwiki
# engine : doku
# shortcut : uw
# base_url : 'http://doc.ubuntu-fr.org'
locales:
en : English
ar : العَرَبِيَّة (Arabic)
bg : Български (Bulgarian)
cs : Čeština (Czech)
da : Dansk (Danish)
de : Deutsch (German)
el_GR : Ελληνικά (Greek_Greece)
eo : Esperanto (Esperanto)
es : Español (Spanish)
fi : Suomi (Finnish)
fil : Wikang Filipino (Filipino)
fr : Français (French)
he : עברית (Hebrew)
hr : Hrvatski (Croatian)
hu : Magyar (Hungarian)
it : Italiano (Italian)
ja : 日本語 (Japanese)
nl : Nederlands (Dutch)
pl : Polski (Polish)
pt : Português (Portuguese)
pt_BR : Português (Portuguese_Brazil)
ro : Română (Romanian)
ru : Русский (Russian)
sk : Slovenčina (Slovak)
sl : Slovenski (Slovene)
sr : српски (Serbian)
sv : Svenska (Swedish)
tr : Türkçe (Turkish)
uk : українська мова (Ukrainian)
zh : 中文 (Chinese)
zh_TW : 國語 (Taiwanese Mandarin)
doi_resolvers :
oadoi.org : 'https://oadoi.org/'
doi.org : 'https://doi.org/'
doai.io : 'https://doai.io/'
sci-hub.tw : 'https://sci-hub.tw/'
default_doi_resolver : 'sci-hub.tw'

View File

@ -7,28 +7,20 @@
./packages.nix ./packages.nix
./jobs.nix ./jobs.nix
./matrix.nix ./matrix.nix
./email.nix
./magnetico.nix ./magnetico.nix
./nameserver.nix ./nameserver.nix
./custom ./custom
./secrets ./secrets
./fish.nix
./neovim.nix
]; ];
### State ### State
# Stateful things to do before updating: # Stateful things to do before updating:
# 1. Postgres migration (https://www.postgresql.org/docs/current/upgrading.html) # 1. Postgres migration
# 2. Matrix Synapse migration (https://matrix-org.github.io/synapse/latest/upgrade.html) # 2. Matrix Synapse migration
system.stateVersion = "23.05"; system.stateVersion = "20.03";
nixpkgs.source = builtins.fetchTarball
{ url = "https://github.com/NixOS/nixpkgs/archive/3f0a8ac25fb6.tar.gz";
sha256 = "10i7fllqjzq171afzhdf2d9r1pk9irvmq5n55h92rc47vlaabvr4";
};
boot.kernelPackages = pkgs.linuxPackages_latest; boot.kernelPackages = pkgs.linuxPackages_latest;
boot.tmp.useTmpfs = true; boot.tmpOnTmpfs = true;
boot.kernel.sysctl = { boot.kernel.sysctl = {
# avoid OOM hangs # avoid OOM hangs
"vm.admin_reserve_kbytes" = 262144; "vm.admin_reserve_kbytes" = 262144;
@ -38,41 +30,34 @@
i18n.defaultLocale = "en_US.UTF-8"; i18n.defaultLocale = "en_US.UTF-8";
systemd.enableEmergencyMode = false; systemd.enableEmergencyMode = false;
systemd.oomd.enable = false;
networking = { networking = {
hostName = "maxwell"; hostName = "maxwell";
firewall.allowedTCPPorts = [ firewall.allowedTCPPorts = [
53 # dns 443 80 # reverse proxy
443 80 # reverse proxy 8080 # hubot
993 # imaps server 5349 # turn server
25 465 # smtp(s) server 5350 # turn server
3551 # apcups
5001 # iperf server
18080 # monero p2p 18080 # monero p2p
22000 # syncthing transfer 20000 # syncthing transfert
64738 # mumble server 64738 # mumble server
]; ];
firewall.allowedUDPPorts = [ firewall.allowedUDPPorts = [
500 # ipsec 53 # powerdns
53 # dns 1194 # dnscrypt
21027 # syncthing discovery 21027 # syncthing discovery
64738 # mumble server 64738 # mumble server
]; ];
firewall.allowedUDPPortRanges = [
nftables.enable = true; { from=49152; to=49999; } # turn relay
firewall.extraInputRules = '' ];
meta l4proto esp counter accept comment "allow IPsec"
ip saddr 192.168.1.0/24 tcp dport apcupsd accept comment "allow UPS from LAN"
'';
usePredictableInterfaceNames = false; usePredictableInterfaceNames = false;
nameservers = [ "127.0.0.1" ]; nameservers = [ "127.0.0.1" ];
hosts."127.0.0.1" = [ config.var.hostname ];
# ensure hostname works without DNS
hosts = with config.var;
{ ${ipv4LanAddress} = [ hostname ];
${ipv6Address} = [ hostname ];
};
}; };
# Only declarative users and no password logins # Only declarative users and no password logins
@ -80,7 +65,7 @@
users.users ={ users.users ={
# Only needed for local (read emergency) shell access # Only needed for local (read emergency) shell access
root.hashedPasswordFile = config.secrets.passwords.root; root.passwordFile = config.secrets.passwords.root;
# Admin # Admin
rnhmjoj = { rnhmjoj = {
@ -95,11 +80,12 @@
fazo = { fazo = {
extraGroups = [ "wheel" ]; extraGroups = [ "wheel" ];
isNormalUser = true; isNormalUser = true;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.fazo ]; openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.fazo];
}; };
# User # Runs two chatbots
meme = { meme = {
extraGroups = [ "ubino" "miguelbridge" ];
isNormalUser = true; isNormalUser = true;
shell = pkgs.fish; shell = pkgs.fish;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.meme ]; openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.meme ];
@ -117,26 +103,19 @@
builder = { builder = {
description = "Remote Nix builds user"; description = "Remote Nix builds user";
isNormalUser = true; isNormalUser = true;
openssh.authorizedKeys.keyFiles = with config.secrets.publicKeys; [ openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.rnhmjoj-builder ];
rnhmjoj-builder
giu-builder
];
}; };
# Use "git" instead of the default name to make # Use "git" instead of the default name to make
# SSH operation handier, example: # SSH operation handier, example:
# git clone git@maxwell:user/repo # git clone git@maxwell:user/repo
git = { git = {
group = "git";
description = "Git server user"; description = "Git server user";
home = "/var/lib/gitea"; home = "/var/lib/gitea";
isSystemUser = true;
useDefaultShell = true; useDefaultShell = true;
}; };
}; };
users.groups.git = { };
# Generate Diffie-Hellman parameters # Generate Diffie-Hellman parameters
# for TLS applications, like nginx. # for TLS applications, like nginx.
security.dhparams = { security.dhparams = {
@ -148,57 +127,64 @@
enable = true; enable = true;
# Users don't have a password # Users don't have a password
wheelNeedsPassword = false; wheelNeedsPassword = false;
extraConfig =
let
path = "/run/current-system/sw/bin";
journal = name: "${path}/journalctl -* ${name}";
services = lib.concatMapStringsSep "," (name: "${journal name}");
in ''
# Allow meme to see his logs.
Cmnd_Alias MEME_UNITS = ${services ["ubino" "miguelbridge"]}
meme ALL=(root) NOPASSWD: MEME_UNITS
'';
}; };
security.pam.loginLimits = [ security.polkit.extraConfig = ''
# Limit user process to stop fork bombs // Allow meme to manage his services.
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.user == "meme" &&
(action.lookup("unit") == "ubino.service" ||
action.lookup("unit") == "miguelbridge.service")) {
return polkit.Result.YES;
}
});
'';
# Limit user process to stop fork bombs
security.pam.loginLimits = [
{ domain = "@users"; { domain = "@users";
type = "hard"; type = "hard";
item = "nproc"; item = "nproc";
value = "400"; value = "400";
} }
# Disable core dumping
{ domain = "*";
type = "soft";
item = "core";
value = "0";
}
]; ];
### ACME certificates ### ACME certificates
security.acme = { security.acme = with config.var; {
defaults.email = "rnhmjoj@inventati.org"; email = "rnhmjoj@inventati.org";
acceptTerms = true; acceptTerms = true;
certs."maxwell.eurofusion.eu" = { certs."${hostname}" = {
group = "maxwell-eurofusion-eu"; group = "maxwell-ydns-eu";
}; };
certs."eurofusion.eu" = { certs."riot.${hostname}" = {
group = "eurofusion-eu"; group = "riot-maxwell-ydns-eu";
}; };
}; };
# Allow read access to ACME certificate # Allow read access to ACME certificate
# to specific (service) users. # to specific (service) users.
users.groups."maxwell-eurofusion-eu".members = [ "murmur" "nginx" ]; users.groups."maxwell-ydns-eu".members = [ "murmur" "turnserver" ];
users.groups."eurofusion-eu".members = [ "nginx" ]; users.groups."riot-maxwell-ydns-eu".members = [ "nginx" ];
# sensible logging
services.journald = {
storage = "volatile";
extraConfig = ''
RuntimeMaxUse=2G
'';
};
services.openssh = { services.openssh = {
enable = true; enable = true;
settings.PermitRootLogin = "no"; permitRootLogin = "no";
settings.PasswordAuthentication = false; passwordAuthentication = false;
settings.KbdInteractiveAuthentication = false; challengeResponseAuthentication = false;
}; };
# Traceroute easter egg # Traceroute easter egg
@ -215,19 +201,21 @@
### Mumble server ### Mumble server
services.murmur = { services.murmur = {
enable = true; enable = true;
password = "allwellthatmaxwell";
registerHostname = config.var.hostname; registerHostname = config.var.hostname;
registerName = "Maxwell Mumble"; registerName = "Maxwell Mumble";
registerPassword = "$REG_PASSWORD"; registerPassword = config.secrets.murmur.password;
password = "$JOIN_PASSWORD";
users = 10; users = 10;
environmentFile = config.secrets.environments.murmur; extraConfig = with config.var; ''
sslCert = "/var/lib/acme/${config.var.hostname}/fullchain.pem"; sslCert=/var/lib/acme/${hostname}/fullchain.pem
sslKey = "/var/lib/acme/${config.var.hostname}/key.pem"; sslKey=/var/lib/acme/${hostname}/key.pem
'';
}; };
### Syncthing node ### Syncthing node
services.syncthing = { services.syncthing = {
enable = true; enable = true;
openDefaultPorts = true;
}; };
### Monero node with local RPC ### Monero node with local RPC
@ -260,88 +248,26 @@
### Git server ### Git server
services.gitea = with config.var; { services.gitea = with config.var; {
enable = true; enable = true;
domain = hostname;
appName = "Maxwell git server"; appName = "Maxwell git server";
rootUrl = "https://${hostname}/git/";
user = "git"; user = "git";
database.user = "git"; database.user = "git";
log.level = "Error";
cookieSecure = true;
disableRegistration = false;
settings = { settings = {
server.ROOT_URL = "https://${hostname}/git/";
server.domain = hostname;
session.COOKIE_SECURE = true;
log.LEVEL = "Error";
service.DISABLE_REGISTRATION = false;
# increase cookie expiration time
security.LOGIN_REMEMBER_DAYS = 365; security.LOGIN_REMEMBER_DAYS = 365;
# file upload size (MB)
attachment.MAX_SIZE = 10; attachment.MAX_SIZE = 10;
# new users can only create PR/issues
service.DEFAULT_ALLOW_CREATE_ORGANIZATION = false;
repository.MAX_CREATION_LIMIT = 0;
# somewhat limit spam
service.EMAIL_DOMAIN_BLOCKLIST = "gmail.com";
# allow the notify webhook to use matrix
webhook.ALLOWED_HOST_LIST = "maxwell.eurofusion.eu";
}; };
}; };
### Searx instance ### Searx instance
services.searx = { services.searx = {
enable = true; enable = true;
environmentFile = config.secrets.environments.searx; configFile = ./assets/searx-settings.yml;
package = pkgs.searxng;
# Use nginx+uWSGI
runInUwsgi = true;
uwsgiConfig = {
disable-logging = true;
# serve using the uwsgi protocol
socket = "/run/searx/uwsgi.sock";
chmod-socket = "660";
# use /searx as url "mountpoint"
mount = "/srx=searx.webapp:application";
module = "";
manage-script-name = true;
# caching
cache2 = lib.concatStringsSep ","
[ "name=searxcache"
"items=2000"
"blocks=2000"
"blocksize=4096"
"bitmap=1"
];
};
settings =
{ general.instance_name = "searxwell";
server.base_url = "https://${config.var.hostname}/";
server.secret_key = "@SEARX_SECRET@";
# Replace DOI links with Sci-Hub
default_doi_resolver = "sci-hub.st";
## Use authenticated APIs for some services
engines = [
{ name = "wolframalpha";
api_key = "@WOLFRAM_API_KEY@";
}
{ name = "youtube";
api_key = "@YOUTUBE_API_KEY@";
}
];
};
}; };
# Allow nginx access to the uwsgi socket
users.groups."searx".members = [ "nginx" ];
### Reverse Proxy ### Reverse Proxy
services.nginx = services.nginx =
@ -355,13 +281,21 @@
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
''; '';
in in
rec { {
enable = true; enable = true;
enableReload = true; enableReload = true;
recommendedTlsSettings = true; commonHttpConfig = ''
# recommendedTlsSettings = true;
# android doesn't like this one:
# ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:42m;
ssl_session_timeout 23m;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
'';
recommendedGzipSettings = true; recommendedGzipSettings = true;
recommendedProxySettings = true; recommendedProxySettings = true;
appendHttpConfig = disableLog;
# Large enough to allow file uploads. # Large enough to allow file uploads.
clientMaxBodySize = "1000M"; clientMaxBodySize = "1000M";
@ -369,51 +303,38 @@
sslDhparam = "${config.security.dhparams.path}/nginx.pem"; sslDhparam = "${config.security.dhparams.path}/nginx.pem";
# Maxwell # Maxwell
virtualHosts."${hostname}" = { virtualHosts."${hostname}" =
enableACME = true; {
forceSSL = true; enableACME = true;
default = true; forceSSL = true;
extraConfig = enableSTS; default = true;
extraConfig = disableLog + enableSTS;
# Returns IP address # Returns IP address
locations."/ip".extraConfig = '' locations."/ip".extraConfig = "return 200 $remote_addr;";
default_type text/plain;
return 200 $remote_addr;
'';
# Asjon code coverage reports # Asjon code coverage reports
locations."/asjon/report/" = { locations."/asjon/report/" = {
index = "index.html"; index = "index.html";
alias = "/run/nginx/static/asjon/"; alias = "/var/lib/asjon/tree/report/";
}; };
# Searx instance # Searx instance
locations."/srx/".extraConfig = locations."/srx/" = {
'' proxyPass = "http://localhost:8083/";
include ${pkgs.nginx}/conf/uwsgi_params; extraConfig = ''
uwsgi_pass unix:/run/searx/uwsgi.sock; proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /srx/;
proxy_buffering off;
''; '';
locations."/srx/static/".alias = "${config.services.searx.package}/share/static/"; };
# Git server # Git server
locations."/git/".proxyPass = "http://localhost:3000/"; locations."/git/" .proxyPass = "http://localhost:3000/";
# Syncthing # Syncthing
locations."/sync/".proxyPass = "http://localhost:8384/"; locations."/sync/".proxyPass = "http://localhost:8384/";
# User static files
locations."/~rnhmjoj/" = {
alias = "/run/nginx/static/rnhmjoj/";
extraConfig = ''
charset UTF-8;
# directories with listing
location ~ /~rnhmjoj/[^/]+.index/ { autoindex on; }
'';
};
locations."/~giu/" = {
alias = "/run/nginx/static/giu/";
extraConfig = "charset UTF-8;";
};
}; };
# Breve URL shortner # Breve URL shortner
@ -431,82 +352,87 @@
# The Cactalogue # The Cactalogue
virtualHosts."cacta.bit" = { virtualHosts."cacta.bit" = {
root = "/run/nginx/static/cactalogue"; locations."/".alias = "/home/giu/cactalogue/";
extraConfig = disableLog; extraConfig = disableLog;
}; };
virtualHosts."cacta.eurofusion.eu" = virtualHosts."cacta.bit";
}; };
# Bind mount directories for Nginx
# This avoids giving nginx traversal permission
systemd.mounts =
let bindNginx = from: to:
{ what = from;
where = "/run/nginx/static/" + to;
type = "none";
options = "bind";
wantedBy = [ "nginx.service" ];
};
in [ (bindNginx "/home/rnhmjoj/www" "rnhmjoj")
(bindNginx "/home/giu/www" "giu")
(bindNginx "/home/giu/cactalogue" "cactalogue")
(bindNginx "/var/lib/asjon/tree/report" "asjon")
];
### IPsec mesh
environment.etc."ipsec.d/mesh.secrets".source = config.secrets.passwords.mesh;
services.libreswan.enable = true;
services.libreswan.connections.mesh =
''
leftid=@wes
left=2a01:e11:1001:53ea::1
rightid=@maxwell
right=${config.var.ipv6Address}
authby=secret
type=transport
auto=ondemand
failureshunt=drop
negotiationshunt=hold
'';
### Misc. services ### Misc. services
services.ubino.enable = true;
services.miguelbridge.enable = true;
services.asjon.enable = true; services.asjon.enable = true;
# Needed for the Asjon memory module # Needed for the Asjon memory module
services.redis.servers."asjon" = services.redis.enable = true;
{ enable = true;
user = "asjon";
};
# Emergency SSH access via tor
services.tor =
{ enable = true;
client.enable = false;
relay.onionServices.emergency-access.map = [ 22 ];
};
nix.settings = { ### Program configuration
# Can connect to the Nix daemon programs = {
# and upload/run code as root! fish.enable = true;
trusted-users = [ "builder" "rnhmjoj" ]; mosh.enable = true;
# Use at most half the cores tmux = {
cores = 8; enable = true;
max-jobs = 16; newSession = true;
# Always keep at least 256MiB free baseIndex = 1;
min-free = 268435456; escapeTime = 0;
historyLimit = 4096;
keyMode = "vi";
terminal = "screen-256color";
customPaneNavigationAndResize = true;
extraConfig = ''
set -g mouse on
# bindings
bind | split-window -h
bind - split-window -v
bind : command-prompt
bind -n C-k clear-history
# colors
set -g pane-border-style fg=brightblack
set -g pane-active-border fg=green
set -g message-style fg=white,bg=black
set -g status-style fg=brightblue,bg=black
setw -g mode-style fg=black,bg=cyan
# status line
set -g status on
set -g status-justify left
set -g status-left ""
set -g status-right-length 60
set -g status-right '#[fg=yellow]#(cut -d\ -f 1-3 /proc/loadavg) | #[fg=brightgreen]%a %H:%M'
setw -g window-status-format "#[fg=black#,bg=brightblack] #I #[fg=blue#,bg=black] #W "
setw -g window-status-current-format "#[fg=white#,bg=cyan] #I #[fg=black#,bg=brightblack] #W "
'';
};
}; };
environment.sessionVariables = { nix = {
PATH = [ "$HOME/bin" ]; useSandbox = true;
XDG_CONFIG_HOME = "$HOME/etc"; # Can connect to the Nix daemon
XDG_DATA_HOME = "$HOME/var/lib"; # and upload/run code as root!
XDG_CACHE_HOME = "$HOME/var/cache"; trustedUsers = [ "builder" "rnhmjoj" ];
SYSTEMD_COLORS = "16"; # Use at most half the cores
buildCores = 8;
extraOptions = ''
# Always keep at least 256MiB free
min-free = 268435456
'';
};
environment.variables = {
PATH = "$HOME/.local/bin/:$PATH";
XDG_CONFIG_HOME = "$HOME/.config";
XDG_DATA_HOME = "$HOME/.local/share";
XDG_CACHE_HOME = "$HOME/.cache";
NIX_PROFILE = "$XDG_CONFIG_HOME/nix/profile";
};
# Needed to make the mosh server survive a
# user logout: systemd kills everything by default
environment.shellAliases = {
mosh-server = "systemd-run --user --scope mosh-server";
}; };
} }

View File

@ -7,12 +7,11 @@
[ # Misc. system services [ # Misc. system services
./modules/breve.nix ./modules/breve.nix
./modules/asjon.nix ./modules/asjon.nix
./modules/ubino.nix
./modules/miguelbridge.nix
# Safely handle secrets # Safely handle secrets
./modules/secrets-store.nix ./modules/secrets-store.nix
# Pin Nixpkgs
./modules/nixpkgs.nix
]; ];
} }

View File

@ -27,33 +27,21 @@ in {
''; '';
}; };
group = mkOption {
type = types.str;
default = "asjon";
description = ''
Asjon will be run under this group (user will be created if it doesn't exist.
This can be your user name).
'';
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
users.users.${cfg.user} = { users.extraUsers."${cfg.user}" = {
group = cfg.group;
home = cfg.dataDir; home = cfg.dataDir;
isSystemUser = true;
createHome = true; createHome = true;
description = "asjon user"; description = "asjon user";
shell = "${pkgs.bash}/bin/bash"; shell = "${pkgs.bash}/bin/bash";
}; };
users.groups.${cfg.group} = { };
systemd.services.asjon = { systemd.services.asjon = {
description = "asjon: our chat bot"; description = "asjon: our chat bot";
after = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ]; after = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
partOf = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ]; requires = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
path = with pkgs; [ path = with pkgs; [
@ -73,12 +61,11 @@ in {
# Scripts # Scripts
AUTO_KILL_ON_UPDATE = "1"; AUTO_KILL_ON_UPDATE = "1";
AUTO_INFORM_ON_START = "!XQJXsOXfTevAiEbDTA:eurofusion.eu"; AUTO_INFORM_ON_START = "!kvLvoCovzInhiablSq:maxwell.ydns.eu";
ADMIN_ROOM = "!XQJXsOXfTevAiEbDTA:eurofusion.eu"; ADMIN_ROOM = "!kvLvoCovzInhiablSq:maxwell.ydns.eu";
REV_REMOTE_HOST = "proxy@rnhmjoj.ydns.eu"; REV_REMOTE_HOST = "proxy@rnhmjoj.ydns.eu";
REV_REMOTE_PORT = "22"; REV_REMOTE_PORT = "22";
REV_KEY = "~/.ssh/proxy"; REV_KEY = "~/.ssh/proxy";
REDIS_URL = "redis:///run/redis-asjon/redis.sock";
}; };
serviceConfig = { serviceConfig = {
@ -87,7 +74,7 @@ in {
Restart = "always"; Restart = "always";
WorkingDirectory = "${cfg.dataDir}/tree"; WorkingDirectory = "${cfg.dataDir}/tree";
# API keys and passwords definitions # API keys and passwords definitions
EnvironmentFile = config.secrets.environments.asjon; EnvironmentFile = config.secrets.asjon.environment;
}; };
}; };
@ -109,6 +96,11 @@ in {
git clone https://github.com/rnhmjoj/asjon.git ${cfg.dataDir}/tree git clone https://github.com/rnhmjoj/asjon.git ${cfg.dataDir}/tree
cd ${cfg.dataDir}/tree cd ${cfg.dataDir}/tree
yarn install yarn install
# give read/traverse permission to nginx
setfacl -m g:nginx:x ${cfg.dataDir}
setfacl -m g:nginx:x ${cfg.dataDir}/tree
setfacl -Rdm g:nginx:rx ${cfg.dataDir}/tree/report
''; '';
}; };

View File

@ -44,15 +44,6 @@ in {
''; '';
}; };
group = mkOption {
type = types.str;
default = "breve";
description = ''
Breve will run under this group (user will be created if it doesn't exist.
This can be your user name).
'';
};
hostname = mkOption { hostname = mkOption {
type = types.str; type = types.str;
default = config.networking.hostName; default = config.networking.hostName;
@ -107,14 +98,11 @@ in {
config = mkIf cfg.enable { config = mkIf cfg.enable {
users.users.${cfg.user} = { users.extraUsers."${cfg.user}" = {
isSystemUser = true; isSystemUser = true;
group = cfg.group;
description = "Breve daemon user"; description = "Breve daemon user";
}; };
users.groups.${cfg.group} = {};
networking.firewall = mkIf cfg.openPorts { networking.firewall = mkIf cfg.openPorts {
allowedTCPPorts = [ cfg.port ] allowedTCPPorts = [ cfg.port ]
++ optional (cfg.port == 443) 80; ++ optional (cfg.port == 443) 80;
@ -128,7 +116,6 @@ in {
environment.XDG_CONFIG_HOME = "${dataDir}/conf"; environment.XDG_CONFIG_HOME = "${dataDir}/conf";
serviceConfig = { serviceConfig = {
User = cfg.user; User = cfg.user;
Group = cfg.group;
ExecStart = "${pkgs.haskellPackages.breve}/bin/breve"; ExecStart = "${pkgs.haskellPackages.breve}/bin/breve";
Restart = "on-failure"; Restart = "on-failure";
StateDirectory = "breve"; StateDirectory = "breve";

View File

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.miguelbridge;
in {
options.services.miguelbridge = {
enable = mkEnableOption "miguelbridge: Bridge Telegram - Matrix.";
user = mkOption {
type = types.str;
default = "miguelbridge";
description = ''
miguelbridge will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.groups.miguelbridge = {};
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = "miguelbridge";
description = "miguelbridge user";
};
systemd.services.miguelbridge = {
description = "miguelbridge: Bridge Telegram - Matrix";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = "miguelbridge";
ExecStart = "${pkgs.openjdk}/bin/java -jar MiguelBridge.jar";
Restart = "always";
StateDirectory = "miguelbridge";
WorkingDirectory = "%S/miguelbridge";
};
};
};
}

View File

@ -1,39 +0,0 @@
{ config, lib, ... }:
let
nixpkgs = config.nixpkgs.source;
conf = "${toString ../..}/configuration.nix";
rebuild = self: super:
{ nixos-rebuild = super.nixos-rebuild.overrideAttrs (old:
{ postInstall = old.postInstall +
''
sed -i "$target" \
-e '/^export PATH/ a \
export NIX_PATH="nixpkgs=${nixpkgs}:nixos-config=${conf}"' \
-e 's/remoteSudo=/remoteSudo=1/' \
-e 's/-A system/-A system --no-out-link/'
'';
});
};
in
{
options.nixpkgs.source = lib.mkOption
{ type = lib.types.path;
description = "Nixpkgs sources";
};
config =
{ nixpkgs.overlays = [ rebuild ];
nix.nixPath =
[ "nixpkgs=/run/current-system/nixpkgs"
"nixos-config=${conf}"
];
system.extraSystemBuilderCmds = "ln -s ${nixpkgs} $out/nixpkgs";
};
}

View File

@ -5,8 +5,6 @@ with lib;
let let
cfg = config.security.runtimeSecrets; cfg = config.security.runtimeSecrets;
secretsStore = "/var/secrets";
# A recursive attrset of submodule # A recursive attrset of submodule
storeType = types.attrsOf (types.submodule storeType = types.attrsOf (types.submodule
{ freeformType = storeType; { freeformType = storeType;
@ -51,24 +49,23 @@ let
let index = name: value: let index = name: value:
if isAttrs value && cond value if isAttrs value && cond value
then recurse (path ++ [name]) value then recurse (path ++ [name]) value
else singleton { loc = path ++ [name]; value = value; }; else singleton { path = path ++ [name]; value = value; };
in concatLists (mapAttrsToList index set); in concatLists (mapAttrsToList index set);
in recurse [] set; in recurse [] set;
isFile = v: isAttrs v && v.path != ""; isFile = v: isAttrs v && v.path != "";
# Secret files flattened to an index. This is needed # Secret files flattened to an index. This is needed
# to iterate over the set. It contains: {name, path, value} # to iterate over the set.
secretFiles = secretFiles =
(map (x: x // { name = concatStringsSep "-" x.loc; }) filter (pair: isFile pair.value)
(filter (pair: isFile pair.value) (attrsToIndex (v: !isFile v) cfg);
(attrsToIndex (v: !isFile v) cfg)));
# Secrets with paths rewritten to the store location # Secrets with paths rewritten to the store location
storedSecrets = mapAttrsRecursiveCond (v: !isFile v) storedSecrets = mapAttrsRecursiveCond (v: !isFile v)
(names: secret: (names: secret:
if isFile secret if isFile secret
then "${secretsStore}/${concatStringsSep "-" names}" then "/run/secrets/${concatStringsSep "-" names}"
else secret) cfg; else secret) cfg;
in { in {
@ -79,7 +76,7 @@ in {
Definitions of runtime secrets. This is a freeform attributes Definitions of runtime secrets. This is a freeform attributes
set: it can contain arbitrarily nested sets of secrets. set: it can contain arbitrarily nested sets of secrets.
Secrets are paths to be copied into the secrets store Secrets are paths to be copied into the secrets store
(${secretsStore}) with proper permission and ownership. (/run/secrets) with proper permission and owenership.
''; '';
}; };
@ -112,28 +109,33 @@ in {
deps = [ ]; deps = [ ];
text = text =
'' ''
secret=${(head secretFiles).value.path} echo setting up secrets store...
if test -f "$secret"; then rm -rf /run/secrets
echo copying secrets... '' + concatMapStrings (pair:
rm -rf ${secretsStore} let
${concatMapStrings (f: '' name = "${concatStringsSep "-" pair.path}";
install -m ${f.value.mode} -D ${f.value.path} ${secretsStore}/${f.name} secret = pair.value;
'') secretFiles} in
fi ''
''; # Install secret ${name}
install -m ${secret.mode} -D ${secret.path} /run/secrets/${name}
'') secretFiles;
}; };
# Set secrets ownership, later because the # Set secrets ownership, later because the
# `user` activation script hasn't run yet. # `user` activation script hasn't run yet.
config.system.activationScripts.secrets-own = { config.system.activationScripts.secrets-own = {
deps = [ "users" "groups" ]; deps = [ "secrets-copy" "users" ];
text = text = concatMapStrings (pair:
'' let
echo setting secrets ownership... name = "${concatStringsSep "-" pair.path}";
${concatMapStrings (f: '' secret = pair.value;
chown ${f.value.user}:${f.value.group} ${secretsStore}/${f.name} in
'') secretFiles} ''
''; echo setting secrets store ownership...
# Set ownership of ${name}
chown ${secret.user}:${secret.group} /run/secrets/${name}
'') secretFiles;
}; };
} }

52
custom/modules/ubino.nix Normal file
View File

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ubino;
in {
options.services.ubino = {
enable = mkEnableOption "Ubino: assistente virtuale di Ube, sottoforma di bot di Telegram.";
user = mkOption {
type = types.str;
default = "ubino";
description = ''
Ubino will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.groups.ubino = {};
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = "ubino";
description = "Ubino user";
};
systemd.services.ubino = {
description = "Ubino: assistente virtuale di Ube, sottoforma di bot di Telegram.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = "ubino";
ExecStart = "${pkgs.openjdk}/bin/java -jar UbinoBot.jar";
Restart = "always";
StateDirectory = "ubino";
WorkingDirectory = "%S/ubino";
};
};
};
}

View File

@ -1,35 +1,20 @@
{ lib { writeScriptBin, fish, curl
, writers
, curl
, jq
, homeserver , homeserver
, roomId , roomId
, authToken , authToken
}: }:
writers.writeDashBin "notify" '' writeScriptBin "notify" ''
export PATH="$PATH:${lib.makeBinPath [ curl jq ]}" #!${fish}/bin/fish
if test $(id -u) != 0; then set token (cat ${authToken})
if test (id -u) != 0
echo 'you must be root to send a notice' echo 'you must be root to send a notice'
exit 1 exit 1
fi end
token=$(cat ${authToken}) set url '${homeserver}/rooms/${roomId}/send/m.room.message?access_token='$token
url="${homeserver}/rooms/${roomId}/send/m.room.message?access_token=$token" set msg '{"msgtype":"m.text", "body": "'$argv[1]'"}'
${curl}/bin/curl -s -XPOST -d $msg $url
if test $# -eq 1; then
# send first arg as text
msg=$(printf "%s" "$1" | jq -Rsc '{ "msgtype": "m.text", "body": . }')
else
# send stdin formatted as code
msg=$(jq -Rsc '{
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": "",
"formatted_body": ("<pre><code>" + . + "</code></pre>")
}')
fi
curl -s "$url" -d "$msg"
'' ''

View File

@ -1,96 +0,0 @@
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball {
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-24.05/nixos-mailserver-nixos-24.11.tar.gz";
sha256 = "08zdidja5kdqgskynxsmcd8skh1b7cfl9ijjy9pak4b5h3aw2iqv";
})
];
mailserver = {
enable = true;
fqdn = "mail.eurofusion.eu";
domains = [ "eurofusion.eu" ];
messageSizeLimit = 78643200; # ~50MiB of base64 binary
loginAccounts = config.secrets.emailAccounts;
extraVirtualAliases = config.secrets.emailAliases;
# store state under /var
mailDirectory = "/var/lib/mail";
dkimKeyDirectory = "/var/lib/dkim";
mailboxes = {
# default IMAP folders
Sent = { specialUse = "Sent"; auto = "subscribe"; };
Drafts = { specialUse = "Drafts"; auto = "subscribe"; };
Spam = { specialUse = "Junk"; auto = "subscribe"; };
Trash = { specialUse = "Trash"; auto = "no"; };
};
# Use Let's Encrypt certificate
certificateScheme = "acme-nginx";
# There is one already (pdns-recursor)
localDnsResolver = false;
# Enable IMAPS (993), SMTPS (465)
enableImapSsl = true;
enableImap = false;
enableSubmissionSsl = true;
enableSubmission = false;
};
services.dovecot2.extraConfig = ''
# Improve hashing speed
auth_cache_verify_password_with_worker = yes
'';
services.postfix.extraConfig = ''
# Prefer IPv6
smtp_address_preference = ipv6
# Prevent binding on temporary addresses
smtp_bind_address6 = ${config.var.ipv6Address}
'';
# Keep the key stable across renewals (for DANE)
security.acme.certs.${config.mailserver.fqdn}.extraLegoRenewFlags = [ "--reuse-key" ];
# Utilities
environment.systemPackages = [
# computes the DANE records
(pkgs.writers.writeDashBin "mailserver-dane" ''
set -e
export PATH=${with pkgs; lib.makeBinPath [ coreutils openssl gawk ]}:$PATH
pubkey_hash() {
openssl x509 -noout -pubkey | \
openssl pkey -pubin -outform DER | \
sha256sum | cut -f1 -d' '
}
fqdn=${config.mailserver.fqdn}
cert="/var/lib/acme/$fqdn/cert.pem"
self=$(awk '{print $0} /END CERT/{exit}' "$cert" | pubkey_hash)
ca=$(awk '{if(keep) print $0} /END CERT/{keep=1}' "$cert" | pubkey_hash)
# main: DANE-EE(3) SPKI(1) SHA2-256(1)
printf '_25._tcp.%s. IN TLSA 3 1 1 %s\n' "$fqdn" "$self"
# fallback: DANE-TA(2) SPKI(1) SHA2-256(1)
printf '_25._tcp.%s. IN TLSA 2 1 1 %s\n' "$fqdn" "$ca"
'')
# computes the DKIM record
(pkgs.writers.writeDashBin "mailserver-dkim" ''
set -e
export PATH=${with pkgs; lib.makeBinPath [ coreutils gawk ]}:$PATH
domain=${builtins.elemAt config.mailserver.domains 0}
raw=$(cat ${config.mailserver.dkimKeyDirectory}/*.txt | tr -d '\n\t' | awk -F'"' '{print $2$4}')
printf 'mail._domainkey.%s IN TXT %s' "$domain" "$raw"
'')
];
}

211
fish.nix
View File

@ -1,211 +0,0 @@
{ ... }:
{
programs.fish.enable = true;
programs.fish.shellAbbrs =
{ e = "nvim";
l = "ls -lh";
ip = "ip -c";
iftop = "iftop -m 70M";
};
programs.fish.shellAliases =
{ namecoin-cli = "namecoin-cli -conf=$XDG_CONFIG_HOME/namecoin"; };
programs.fish.loginShellInit =
''
# Start abduco in ssh
if set -q SSH_CLIENT
# start abduco on ssh
if not set -q ABDUCO_SESSION
exec abduco -A ssh fish
else if test $SHLVL -eq 1
tput rmcup
end
end
'';
programs.fish.interactiveShellInit =
''
## Fish settings
# mixed emacs/vi
fish_hybrid_key_bindings
# kj to normal mode
bind -M insert kj "
if commandline -P;
commandline -f cancel;
else;
set fish_bind_mode default;
commandline -f backward-char repaint-mode;
end"
# fix unquoted URLs
set -U fish_features ampersand-nobg-in-token qmark-noglob
# change default cursor
set fish_cursor_insert underscore
## Color scheme
# syntax highlighting
set fish_color_command green
set fish_color_param normal
set fish_color_comment brcyan
set fish_color_operator purple
set fish_color_escape bryellow
set fish_color_redirection blue
set fish_color_selection --background=black
# completion/history
set fish_pager_color_prefix yellow
set fish_pager_color_description brblue
set fish_pager_color_progress brblack --background=black
set fish_color_search_match --background=black
# man pages colors
# bold, blink stop
set -x LESS_TERMCAP_md (set_color -o bryellow)
set -x LESS_TERMCAP_mb (set_color -u magenta)
set -x LESS_TERMCAP_me (set_color normal)
# standout start/stop
set -x LESS_TERMCAP_so (set_color brblue -b black)
set -x LESS_TERMCAP_se (set_color normal -b normal)
# underline start/stop
set -x LESS_TERMCAP_us (set_color -u brmagenta)
set -x LESS_TERMCAP_ue (set_color normal)
# used default LS_COLORS
eval (dircolors | sed 's/\(\w\+\)=/set \1 /')
## Aliases
# start process and detach
function start
nohup $argv > /dev/null 2>&1 &; disown
end
# start process without network access
function nnet
unshare -nc fish -ic "$argv"
end
# interactively rename files
function vimv
set tmp (mktemp --tmpdir -d vimv.XXX)
if set -q argv[1]
# directory listing
find $argv[1] -maxdepth 1 | sort | cat -n | tee $tmp/before > $tmp/after
else
# read from stdin
cat -n - | tee $tmp/before > $tmp/after
end
$EDITOR $tmp/after
# only print differing lines
awk '
NR==FNR { line=$0; sub($1, "", line); sub(/\s+/, "", line); lines[$1]=line; next }
NR!=FNR { line=$0; sub($1, "", line); sub(/\s+/, "", line);
if (!($1 in lines)) { printf("rm -vr \"%s\"\n", line); next }
if (lines[$1] != line) printf("mv -vin \"%s\" \"%s\"\n", line, lines[$1])
}
' $tmp/after $tmp/before | sh
rm -r $tmp
end
'';
programs.fish.promptInit =
''
# Outputs colored text
function color
set_color $argv[1]
for i in $argv[2..-1]
echo -n $i
end
set_color normal
end
# Git branch info
function git_branch
if not test -f .git/HEAD
return
end
set branch (git rev-parse --abbrev-ref HEAD 2> /dev/null)
if test $status -ne 0
set branch (cut -f 3 -d '/' .git/HEAD)
else if test $branch = HEAD
set branch (head -c 10 .git/HEAD)
end
timeout 0.1 git diff-files --quiet 2>/dev/null
if test $status -eq 1
set branch (color yellow $branch'*')
else
set branch (color green $branch)
end
timeout 0.1 git status --porcelain 2>/dev/null 1>| read untracked
if test -n "$untracked"
set dirty (color red '*')
end
echo " <$branch$dirty>"
end
# Left prompt
function fish_prompt
if fish_is_root_user
set prompt (color blue Λ)
set user (color red (whoami))
else
set prompt (color blue λ)
set user (color green (whoami))
end
if test \( "$LINES" -lt 10 \) -o \( "$COLUMNS" -lt 30 \)
echo "$prompt "
return
end
if set -q SSH_CLIENT
set host (color yellow (hostname))
else
set host (color green (hostname))
end
set git (git_branch)
set path (color cyan (prompt_pwd))
switch $fish_bind_mode
case default
set mode (color blue n)
case insert
set mode (color red i)
case visual
set mode (color yellow v)
end
echo "$user@$host $mode$path$git"
echo "$prompt "
end
# Right prompt
function fish_right_prompt
set code $status
if test $code -ne 0
set exitcode (color red $code" ")
end
if string match -q '/nix*' $PATH[1]
set nix " <"(color blue nix)">"
end
echo "$exitcode$nix"
end
function fish_mode_prompt; end
function fish_greeting; end
'';
}

View File

@ -12,6 +12,7 @@
]; ];
boot.loader.grub = { boot.loader.grub = {
enable = true; enable = true;
version = 2;
device = "/dev/sda"; device = "/dev/sda";
}; };
@ -30,6 +31,7 @@
fsType = "ext4"; fsType = "ext4";
}; };
nix.maxJobs = lib.mkDefault 16;
powerManagement.cpuFreqGovernor = "ondemand"; powerManagement.cpuFreqGovernor = "ondemand";
services.apcupsd = { services.apcupsd = {
@ -38,33 +40,36 @@
UPSTYPE usb UPSTYPE usb
UPSCABLE usb UPSCABLE usb
NETSERVER on NETSERVER on
NISPORT 3551
MINUTES 5 MINUTES 5
''; '';
hooks = hooks =
let let
# Send notifications when something bad happens # Send notifications on the Maxwell
# room when something bad happens.
notify = msg: ''${pkgs.maxwell-notify}/bin/notify "UPS: ${msg}"''; notify = msg: ''${pkgs.maxwell-notify}/bin/notify "UPS: ${msg}"'';
in in
{ {
changeme = notify "replace batteries"; changeme = notify "sostituire le batterie";
battdetach = notify "batteries disconnected"; battdetach = notify "batterie disconnesse";
battattach = notify "batteries reconnected"; battattach = notify "batterie riconnesse";
commfailure = notify "connection lost"; commfailure = notify "connessione persa";
commok = notify "connection enstablished"; commok = notify "connessione ristabilita";
loadlimit = notify "critical battery level (5%)"; loadlimit = notify "livello batterie critico (5%)";
runlimit = notify "critical battery life (5min)"; runlimit = notify "autonomia batterie critico (5min)";
doshutdown = notify "shutting down!"; doshutdown = notify "inizio sequenza di spegnimento";
powerout = notify "main power is out"; powerout = notify "rete elettrica disconnessa";
mainsback = notify "main power is back"; mainsback = notify "rete elettrica riconnessa";
onbattery = notify "batteries connected"; onbattery = notify "attivate batterie";
offbattery = notify "batteries disconnected"; offbattery = notify "disattivate batterie";
emergency = notify "battery malfunction, possible shutdown!"; emergency = notify "malfunzionamento batterie, possibile spegnimento!";
}; };
}; };
services.smartd = services.smartd =
let let
# Send a notification when a disk is starting to fail # Send a notification on the Maxwell
# when a disk is starting to fail.
failHook = with pkgs; writeScript "disk-fail-hook" '' failHook = with pkgs; writeScript "disk-fail-hook" ''
#!/bin/sh #!/bin/sh
${pkgs.maxwell-notify}/bin/notify \ ${pkgs.maxwell-notify}/bin/notify \

204
jobs.nix
View File

@ -4,114 +4,126 @@ with lib;
{ {
systemd.services."notify-failed@" = { systemd.services.ydns = {
description = "notify that %i has failed"; description = "update ydns address record";
scriptArgs = "%i"; after = [ "network-online.target" ];
path = [ pkgs.maxwell-notify ]; startAt = "*:0/30";
script = ''
unit=$1
notify "$unit: failed. last log lines:"
journalctl -u "$unit" -o cat -n 15 | notify
'';
};
systemd.services.backup = serviceConfig.Type = "oneshot";
let serviceConfig.environmentFile = config.secrets.ydns.environment;
saved = pkgs.writeText "backup-saved" ''
/etc/lvm
/var/lib
/home
'';
excluded = pkgs.writeText "backup-excluded" '' path = with pkgs; [ curl cacert gawk iproute ];
/var/lib/systemd environment = {
/var/lib/udisks2 YDNS_HOST = config.var.hostname;
/var/lib/postgresql CURL_CA_BUNDLE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
/var/lib/matrix-synapse/media_store/url_cache
/var/lib/matrix-synapse/media_store/url_cache_thumbnails
'';
in {
description = "system backup";
startAt = "*-*-* 03:00"; # every day at 3:00
onFailure = [ "notify-failed@backup.service" ];
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
PrivateMounts = true;
LimitNOFILE = 65536;
}; };
environment.BUP_DIR = "/mnt/backup";
path = with pkgs; [ bup git util-linux sudo gzip postgresql ];
script = '' script = ''
# mount repository update() {
mount -m -L backup "$BUP_DIR" ret=$(curl -$1 --basic --silent \
-u "$YDNS_USER:$YDNS_PASSWD" \
"https://ydns.io/api/v1/update/?host=$YDNS_HOST&ip=$2" || exit 0)
# init backup, if empty case "$ret" in
! test -e $BUP_DIR/bupindex && bup init ok)
echo "updated successfully: $YDNS_HOST ($2)"
;;
# build indices and save badauth)
while read -r dir; do echo "updated failed: $YDNS_HOST (authentication failed)"
{ ;;
name=$(basename "$dir")
echo indexing $name...
bup index "$dir" --exclude-from="${excluded}"
echo done
echo saving $name... *)
bup save -n "$name" "$dir" || true echo "update failed: $YDNS_HOST ($ret)"
echo done ;;
} || true esac
done < "${saved}" }
# postgresql backup update 4 "$(curl -s -4 https://ydns.io/api/v1/ip)"
dir=/tmp/postgresql update 6 "$(ip addr show mngtmpaddr | awk '/inet6/{print $2; exit}' | cut -d/ -f1)"
mkdir -p "$dir"
echo dumping databases...
sudo -u postgres pg_dumpall > "$dir"/db.bak
echo done
echo saving...
bup index "$dir"
bup save -n postgresql "$dir" --strip-path=/tmp
echo done
echo generating par2 files...
bup fsck -j 8 -g
echo done
# prune backups every week
if test $(( $(date +%s) / 86400 % 7 )) -eq 0; then
echo pruning...
bup prune-older --keep-all-for 6m --keep-monthlies-for 2y --unsafe
echo done
fi
''; '';
}; };
systemd.services.namecoin-update = systemd.services.backup = {
let description = "run system backup";
userFile = with config.services.namecoind; after = [ "network-online.target" ];
pkgs.writeText "namecoin.conf" '' startAt = "weekly";
rpcbind=${rpc.address}
rpcport=${toString rpc.port}
rpcuser=${rpc.user}
rpcpassword=${rpc.password}
'';
in {
description = "update namecoin names";
after = [ "namecoind.service" ];
startAt = "hourly";
onFailure = [ "notify-failed@namecoin-update.service" ];
path = [ pkgs.namecoind ]; serviceConfig.Type = "oneshot";
serviceConfig.Type = "oneshot";
serviceConfig.ExecStart = "${pkgs.haskellPackages.namecoin-update}/bin/namecoin-update ${userFile}"; path = with pkgs; [ bup git nfs-utils ];
};
environment.BUP_DIR = "/mnt/backup";
script = ''
${pkgs.fish}/bin/fish << 'EOF'
set locations \
/etc/lvm \
/etc/nixos \
/var/lib \
/home
set excluded \
/var/lib/alsa \
/var/lib/systemd \
/var/lib/udisks2 \
/var/lib/udev \
/var/lib/postgresql
# mount NFS share
mkdir -p $BUP_DIR
mount.nfs -o nolock 192.168.1.3:/maxwell $BUP_DIR
# check if properly mounted
if not mountpoint -q $BUP_DIR
echo mount failed! 1>&2
exit 1
end
# init backup
if not test -e $BUP_DIR/bupindex
bup init
end
# build indices and copy
for i in $locations
eval bup index $i --exclude=(string join " --exclude=" $excluded)
bup save -n (basename $i) $i
end
# postgresql backup
set dir /var/lib/postgresql-backup
mkdir -p $dir
sudo -u postgres pg_dumpall | gzip > $dir/db.bak
bup index $dir
bup save -n postgresql $dir
rm -rf $dir
umount /mnt/backup
EOF
'';
};
systemd.services.namecoin-update =
let
userFile = with config.services.namecoind;
pkgs.writeText "namecoin.conf" ''
rpcbind=${rpc.address}
rpcport=${toString rpc.port}
rpcuser=${rpc.user}
rpcpassword=${rpc.password}
'';
in {
description = "update namecoin names";
after = [ "namecoind.service" ];
startAt = "hourly";
path = [ pkgs.namecoind ];
serviceConfig.Type = "oneshot";
serviceConfig.ExecStart = "${pkgs.haskellPackages.namecoin-update}/bin/namecoin-update ${userFile}";
};
} }

View File

@ -3,7 +3,7 @@
# Setup: # Setup:
# Maxwell runs the web UI (magneticow) but doesn't # Maxwell runs the web UI (magneticow) but doesn't
# run the crawler (magneticod) because it's too # run the crawler (magneticod) because it's too
# network intensive. The latter is run by Wigfrid, # network intesive. The latter is run by Wigfrid,
# which periodically uploads a sqlite database. # which periodically uploads a sqlite database.
# Once received, Maxwell merges it with the local one. # Once received, Maxwell merges it with the local one.

View File

@ -1,14 +1,31 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with config.var;
let let
domain = "eurofusion.eu"; ### Element (Riot) configuration
conf = with config.var; {
default_server_config."m.homeserver" =
{ base_url = "https://${hostname}";
server_name = "Maxwell";
};
default_server_config."m.identity_server" =
{ base_url = "https://matrix.org"; };
roomDirectory.servers = [ "matrix.org" hostname ];
brand = "Maxwell matrix";
defaultCountryCode = "IT";
showLabsSettings = true;
# Use a trusted Jitsi instance
jitsi.preferredDomain = "jitsi.openspeed.org";
jitsi.externalApiUrl = "https://jitsi.openspeed.org/libs/external_api.min.js";
};
in in
{ {
### Reverse proxy locations ### Reverse proxy locations
services.nginx.virtualHosts."${config.var.hostname}" =
# Setup for well-known on the bare domain
services.nginx.virtualHosts.${domain} =
let let
client = client =
{ "m.homeserver" = { "base_url" = "https://${config.var.hostname}"; }; { "m.homeserver" = { "base_url" = "https://${config.var.hostname}"; };
@ -17,9 +34,6 @@ in
server = { "m.server" = "${config.var.hostname}:443"; }; server = { "m.server" = "${config.var.hostname}:443"; };
in in
{ {
enableACME = true;
forceSSL = true;
# Needed for matrix federation # Needed for matrix federation
locations."/.well-known/matrix/server".extraConfig = '' locations."/.well-known/matrix/server".extraConfig = ''
add_header Content-Type application/json; add_header Content-Type application/json;
@ -33,24 +47,40 @@ in
add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}'; return 200 '${builtins.toJSON client}';
''; '';
# Forward matrix API calls to synapse
locations."/_matrix".proxyPass = "http://localhost:8448";
}; };
# Forward matrix/admin API calls to synapse
services.nginx.virtualHosts.${config.var.hostname} = ### Element/Riot static location
{ locations."/_matrix".proxyPass = "http://localhost:8448"; services.nginx.virtualHosts."riot.${config.var.hostname}" =
locations."/_synapse".proxyPass = "http://localhost:8448"; { enableACME = true;
}; forceSSL = true;
locations."/" =
{ index = "index.html";
alias = (pkgs.element-web.override { inherit conf; }) + "/";
};
};
### Homeserver ### Homeserver
services.matrix-synapse.enable = true; services.matrix-synapse = {
services.matrix-synapse.settings = { enable = true;
server_name = domain; server_name = config.var.hostname;
public_baseurl = "https://${config.var.hostname}/";
# Tell users about our TURN server
turn_uris = [
"turn:${config.var.hostname}:5349?transport=udp"
"turn:${config.var.hostname}:5350?transport=udp"
"turn:${config.var.hostname}:5349?transport=tcp"
"turn:${config.var.hostname}:5350?transport=tcp"
];
# Bind on localhost and used a reverse proxy # Bind on localhost and used a reverse proxy
listeners = [ listeners = [
{ bind_addresses = [ "localhost" ]; { bind_address = "localhost";
port = 8448; port = 8448;
type = "http"; type = "http";
tls = false; tls = false;
@ -64,30 +94,30 @@ in
# Connect to Postrges # Connect to Postrges
database_type = "psycopg2"; database_type = "psycopg2";
database_args = database_args = {
{ user = "matrix-synapse"; user = "matrix-synapse";
database = "matrix-synapse"; database = "matrix-synapse";
}; };
# Make logging less verbose # Make logging less verbose
log_config = pkgs.writeText "synapse-log.yml" '' logConfig = ''
version: 1 version: 1
formatters: formatters:
journal_fmt: journal_fmt:
format: '%(name)s: [%(request)s] %(message)s' format: '%(name)s: [%(request)s] %(message)s'
filters: filters:
context: context:
(): synapse.util.logcontext.LoggingContextFilter (): synapse.util.logcontext.LoggingContextFilter
request: "" request: ""
handlers: handlers:
journal: journal:
class: systemd.journal.JournalHandler class: systemd.journal.JournalHandler
formatter: journal_fmt formatter: journal_fmt
filters: [context] filters: [context]
SYSLOG_IDENTIFIER: synapse SYSLOG_IDENTIFIER: synapse
root: root:
level: WARN level: WARN
handlers: [journal] handlers: [journal]
disable_existing_loggers: False disable_existing_loggers: False
''; '';
@ -95,68 +125,65 @@ in
expire_access_token = true; expire_access_token = true;
event_cache_size = "2K"; event_cache_size = "2K";
max_upload_size = "1000M"; max_upload_size = "1000M";
dynamic_thumbnails = true; turn_user_lifetime = "1d";
# Needed to restrict access to the TURN
# server to only our matrix users.
turn_shared_secret = config.secrets.matrix.turn;
# Needed by the register_new_matrix_user script
registration_shared_secret = config.secrets.matrix.registration;
}; };
# Secrets
services.matrix-synapse.extraConfigFiles =
[
# Password reset via email
# Note: can't be put here, see NixOS/nixpkgs#158605
config.secrets.matrix.email.conf
# Needed by the register_new_matrix_user script
config.secrets.matrix.registration
];
### Database ### Database
services.postgresql.enable = true; services.postgresql.enable = true;
# Create databases on the first run # Create database on the first run
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" '' services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse'; CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0 TEMPLATE template0
LC_COLLATE = "C" LC_COLLATE = "C"
LC_CTYPE = "C"; LC_CTYPE = "C";
CREATE ROLE "mautrix-whatsapp" WITH LOGIN PASSWORD 'whatsapp';
CREATE DATABASE "mautrix-whatsapp" WITH OWNER "mautrix-whatsapp"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
''; '';
### Whatsapp bridge
# allow synapse to read the shared secrets # Handles users behind a NAT,
users.users.matrix-synapse.extraGroups = [ "mautrix-whatsapp" ]; # needed for reliable VoIP.
services.coturn = {
enable = true;
# Only allow users vouched for
# by the Matrix server.
lt-cred-mech = true;
use-auth-secret = true;
static-auth-secret = config.secrets.matrix.turn;
# Use maxwell certificate for TLS
realm = config.var.hostname;
cert = "/var/lib/acme/${config.var.hostname}/fullchain.pem";
pkey = "/var/lib/acme/${config.var.hostname}/key.pem";
# Allow olm for mautrix-whatsapp # Port range for TURN relaying
nixpkgs.config.permittedInsecurePackages = [ "olm-3.2.16" ]; min-port = 49152;
max-port = 49999;
services.mautrix-whatsapp =
{ # Enable TLS
enable = true; secure-stun = true;
serviceDependencies = [ "postgresql.service" ]; no-tcp-relay = false;
settings.appservice =
{ database.type = "postgres"; extraConfig = ''
database.uri = "postgresql:///mautrix-whatsapp?host=/run/postgresql"; external-ip=${config.var.ipAddress}
}; cipher-list=HIGH
settings.bridge = no-loopback-peers
{ encryption = no-multicast-peers
{ allow = true; denied-peer-ip=10.0.0.0-10.255.255.255
default = true; denied-peer-ip=192.168.0.0-192.168.255.255
require = true; allowed-peer-ip=192.168.1.5
}; user-quota=12
permissions = total-quota=1200
{ "eurofusion.eu" = "user"; verbose=true
"@rnhmjoj:eurofusion.eu" = "admin"; '';
}; };
relay.enabled = false;
mute_bridging = true;
};
};
} }

View File

@ -1,51 +1,46 @@
{ config, lib, ... }: { config, ... }:
# Setup: # Setup:
# pdns-recursor on localhost:54 # PDNS recursor on port 53
# dnsdist on port 53 (DNS) # DNSCrypt wrapper on port 1194
# ncdns for Namecoin bit. zone resolution # NCDNS for Namecoin bit. zone resolution
{ {
# Recursive DNS resolver # Recursive DNS resolver
services.pdns-recursor = services.pdns-recursor = {
{ enable = true; enable = true;
# Configures the bit. zone # Configures the bit. zone
resolveNamecoin = true; resolveNamecoin = true;
dns.port = 54; dns.allowFrom = [ "0.0.0.0/0" ];
}; };
# Public DNS resolver # Wrap the local recursive resolver
services.dnsdist = # in DNSCrypt on the default OpenVPN port.
{ enable = true; # This port is chosen because it's usually
extraConfig = '' # not blocked in corporate networks.
-- Listen on IPv6 and IPv4 services.dnscrypt-wrapper = {
setLocal("[::]:53"); addLocal("0.0.0.0:53") enable = true;
address = "0.0.0.0";
-- Allow everything port = 1194;
setACL({"0.0.0.0/0", "::/0"}) providerKey.public = config.secrets.dnscrypt.pub;
providerKey.secret = config.secrets.dnscrypt.sec;
-- Set upstream resolver };
newServer({address="[::1]:54", name="pdns"})
'';
};
# Namecoin resolver # Namecoin resolver
services.ncdns = services.ncdns = {
{ enable = true; enable = true;
# This is currently broken, see ncdns issue: # This is currently broken, see ncdns issue:
# https://github.com/namecoin/ncdns/issues/127 # https://github.com/namecoin/ncdns/issues/127
dnssec.enable = false; dnssec.enable = false;
}; };
# Namecoin daemon with RPC server # Namecoin daemon with RPC server
services.namecoind = services.namecoind = {
{ enable = true; enable = true;
# This are used by the resolver (ncdns) # This are used by the resolver (ncdns)
# to query the blockchain. # to query the blockchain.
rpc.user = config.secrets.namecoin.user; rpc.user = config.secrets.namecoin.user;
rpc.password = config.secrets.namecoin.password; rpc.password = config.secrets.namecoin.password;
}; };
users.users.namecoin.group = "namecoin";
} }

View File

@ -1,481 +0,0 @@
{ config, lib, pkgs, ... }:
let
frameline = pkgs.callPackage (pkgs.fetchFromGitea
{ domain = "maxwell.eurofusion.eu/git";
owner = "rnhmjoj";
repo = "nvim-frameline";
rev = "v0.1.0";
sha256 = "PrTSSoXbu+qtTsJUv81z+MuTUmB1RHLPEWFQQnu6+J8=";
}) { };
plugins = with pkgs.vimPlugins;
[ # UI
undotree gitsigns-nvim
frameline nvim-fzf
# Syntax
playground
vim-pandoc-syntax
nix-queries fortran-queries
(nvim-treesitter.withPlugins (p: with p;
[ bash fish
c fortran haskell
html css
nix python lua
]))
# Misc
vim-fugitive supertab neomake
auto-pairs plenary-nvim
];
pack = pkgs.linkFarm "neovim-plugins"
(map (pkg:
{ name = "pack/${pkg.name}/start/${pkg.name}";
path = toString pkg;
}) (lib.concatMap (p: [p] ++ p.dependencies or []) plugins));
neovim-wrapped = pkgs.runCommand "${pkgs.neovim-unwrapped.name}"
{ nativeBuildInputs = [ pkgs.makeWrapper ]; }
''
mkdir -p "$out"
makeWrapper '${pkgs.neovim-unwrapped}/bin/nvim' "$out/bin/nvim" \
--add-flags "-u ${conf}"
'';
nix-queries = pkgs.writeTextDir "/queries/nix/injections.scm"
''
;; extends
; writeText highlight
(apply_expression function:
(apply_expression function: (_) @_func
argument: (string_expression (string_fragment) @injection.language))
argument: [
(string_expression (string_fragment) @injection.content)
(indented_string_expression (string_fragment) @injection.content)]
(#match? @_func "(^|\\.)writeText(Dir)?$")
(#gsub! @injection.language ".*%.(.*)" "%1")
(#set! injection.combined))
'';
fortran-queries = pkgs.writeTextDir "/queries/fortran/highlights.scm"
''
;; extends
(end_block_construct_statement) @keyword
(implicit_statement) @keyword
'';
conf = pkgs.writeText "init.lua" ''
local opt = vim.opt
local cmd = vim.api.nvim_command
local keymap = vim.keymap.set
local autocmd = vim.api.nvim_create_autocmd
-- Load plugins
opt.packpath = ${builtins.toJSON pack}
opt.runtimepath:prepend(${builtins.toJSON pack})
--
-- Options
--
local cache = os.getenv('XDG_CACHE_HOME')..'/nvim'
opt.directory = cache..'/tmp'
opt.backupdir = cache..'/tmp'
opt.shadafile = cache..'/shada'
opt.undodir = cache..'/undo'
opt.hidden = true -- Hide buffers
opt.mouse = 'a' -- Enable mouse support
opt.ignorecase = true -- Case insensitive search...
opt.smartcase = true -- ...for lowercase terms
opt.splitkeep = 'screen' -- keep text still when splitting
opt.laststatus = 3 -- global statusline
opt.fsync = true -- Sync writes
opt.swapfile = false -- Disable swap files
opt.writebackup = true -- Backup file before overwriting...
opt.backup = false -- ...but delete it on success
opt.undofile = true -- Store all changes
opt.modeline = false -- Disable for Security
opt.showmatch = true -- Highlight matched parenthesis
opt.clipboard = 'unnamedplus' -- Yank to clipboard
-- Files to ignore
opt.wildignore = {
'*.so', '*.hi', '*.a', '*.la', '*.mod',
'*/__pycache__/*',
'*/dist/*',
'*/result/*',
'*/.git/*'
}
opt.shiftwidth = 2 -- Tabs
opt.tabstop = 2 --
opt.expandtab = true --
opt.number = true -- Line numbering
opt.smartindent = true -- Indentation
opt.showmode = false -- Disable printing of mode changes
opt.ruler = false -- Already in statusline
opt.fillchars = {
eob=' ', -- Hide ~ on empty lines
vert='', -- make vertical split sign better
fold=' ', -- Hide . in fold markers
}
opt.foldmethod = "expr" -- Folding
opt.foldexpr = 'nvim_treesitter#foldexpr()' --
opt.foldlevel = 99 -- open by default
opt.foldopen:remove("search") -- don't open when searching
--
-- OSC 52 clipboard for SSH
--
if os.getenv('SSH_TTY') ~= nil then
vim.g.clipboard = {
name = 'OSC 52',
copy = {
['+'] = require('vim.ui.clipboard.osc52').copy('+'),
['*'] = require('vim.ui.clipboard.osc52').copy('*'),
},
paste = {
['+'] = require('vim.ui.clipboard.osc52').paste('+'),
['*'] = require('vim.ui.clipboard.osc52').paste('*'),
},
}
end
--
-- Terminal mode
--
-- Hide some UI elements
autocmd('TermOpen', {pattern='*', command='setlocal nonumber'})
autocmd('TermEnter', {pattern='*', command='set cmdheight=0 laststatus=0'})
autocmd('TermLeave', {pattern='*', command='set cmdheight=1 laststatus=3'})
autocmd('VimResized', {pattern='*', command='wincmd ='})
-- Exit without confirmation
autocmd('TermClose', {pattern='*', command='call feedkeys("\\<CR>")'})
keymap('t', '<C-b>', '<C-\\><C-n>', {noremap=true}) -- Easier escape
keymap('n', '<C-b>', '<Nop>', {noremap=true}) --
keymap('n', '<C-w>-', ':split +term<CR>', {silent=true}) -- Tmux-like moves
keymap('n', '<C-w>|', ':vsplit +term<CR>', {silent=true}) --
keymap('n', '<C-w>t', ':tabnew +term<CR>', {silent=true}) --
keymap('n', '<C-w>c', ':quit<CR>', {silent=true}) --
keymap('n', '<M-h>', '<C-w>h', {noremap=true}) --
keymap('n', '<M-j>', '<C-w>j', {noremap=true}) --
keymap('n', '<M-k>', '<C-w>k', {noremap=true}) --
keymap('n', '<M-l>', '<C-w>l', {noremap=true}) --
--
-- Keybindings
--
vim.g.mapleader = ','
function listToggle()
for _, win in ipairs(vim.fn.getwininfo()) do
if win.loclist == 1 then return cmd('lclose') end
end
cmd('lopen')
end
fzf = require'fzf'
function searchFiles()
local query = [[\( -name .git -o -name __pycache__ -o -path ./dist -o -path ./build \) -prune -o -type f]]
coroutine.wrap(function()
res = fzf.fzf('find '..query, "", {border='none'})
cmd('edit '..res[1])
end)()
end
function searchCommands()
coroutine.wrap(function()
local history = {}
for i = 1, vim.fn.histnr("cmd") do
history[i] = vim.fn.histget("cmd", i)
end
res = fzf.fzf(history, "", {border='none'})
vim.fn.feedkeys(':'..res[1], 't')
end)()
end
keymap('n', '<C-p>', searchFiles, {silent=true}) -- Fuzzy search files
keymap('n', '<C-e>', searchCommands, {silent=true}) -- Fuzzy search command history
keymap('n', '<Leader>u', ':UndotreeToggle<CR>', {silent=true}) -- Toggle UndoTree
keymap('n', '<leader>l', listToggle, {silent=true}) -- Toggle Neomake errors
keymap('i', 'kj', '<ESC>', {noremap=true}) -- Exit with kj
keymap('n', 'o', 'o<ESC>', {noremap=true}) -- Add empty lines
keymap('n', 'O', 'O<ESC>', {noremap=true}) --
keymap('x', 'p', 'p:let @+=@0<CR>', {noremap=true, silent=true}) -- Keep selection after p
keymap('c', 'w!!', 'w !sudo tee >/dev/null %', {silent=true}) -- Save with sudo
--
-- Colors
--
opt.bg = 'light' -- Use dark colors
function color(group, args)
vim.api.nvim_set_hl(0, group, args)
end
-- Source code
color('Cursor', {ctermfg=14})
color('Keyword', {ctermfg=04})
color('Define', {ctermfg=03})
color('Type', {ctermfg=06})
color('Identifier', {ctermfg=13})
color('Constant', {ctermfg=03})
color('Function', {ctermfg=02})
color('Include', {ctermfg=04})
color('Statement', {ctermfg=11})
color('String', {ctermfg=03})
color('Number', {ctermfg=04})
color('Comment', {ctermfg=07})
color('SpecialComment', {ctermfg=15})
color('Operator', {ctermfg='none'})
color('Conceal', {ctermfg='none', ctermbg='none'})
-- Text
color('Title', {ctermfg=4})
color('Special', {ctermfg=12})
color('Delimiter', {ctermfg=1})
color('PandocReferenceURL', {ctermfg=3})
color('PandocCiteKey', {ctermfg=6})
color('PandocTableDelims', {ctermfg=8})
color('texBeginEndName', {ctermfg=2})
color('Error', {ctermfg='none', ctermbg='none', underline=true})
-- Editor UI
color('NonText', {ctermfg=0})
color('LineNr', {ctermfg=8})
color('Pmenu', {ctermfg=12, ctermbg=0})
color('Folded', {ctermfg=7, ctermbg=0, cterm={}})
color('VertSplit', {ctermfg=8, cterm={}})
color('FoldColumn', {ctermfg='none', cterm={}})
color('Visual', {ctermfg='none', ctermbg=0})
color('Search', {ctermfg='none', ctermbg=0})
color('CurSearch', {ctermfg='none', ctermbg=0})
-- Diff mode
color('DiffAdd', {ctermfg=2, ctermbg=0, underline=true})
color('DiffChange', {ctermfg=3, ctermbg=0, underline=true})
color('DiffText', {ctermfg=1, ctermbg=0, underline=true})
color('DiffDelete', {ctermfg=0, ctermbg=1, underline=true})
-- Spelling
color('SpellBad', {ctermfg=1, ctermbg=0, underline=true})
color('SpellCap', {ctermfg=3, underline=true})
-- Neomake
color('NeomakeWarning', {ctermfg=3, underline=true})
color('ErrorMsg', {ctermfg=1, ctermbg='none'})
color('WarningMsg', {ctermfg=3})
-- Git signs
color('SignColumn', {ctermfg=1, ctermbg='none', cterm={}})
color('GitSignsAdd', {ctermfg=2})
color('GitSignsChange', {ctermfg=3})
color('GitSignsDelete', {ctermfg=1})
-- Statusline
color('StatusLine', {ctermfg=8, ctermbg=0, cterm={}})
color('StatusLineNC', {ctermfg=4, ctermbg=0, cterm={}})
color('User1', {ctermfg=8, ctermbg=0}) -- base
color('User2', {ctermfg=3, ctermbg=0}) -- location
color('StatusLineErr', {ctermfg=0, ctermbg=1})
color('StatusLineWarn', {ctermfg=0, ctermbg=3})
color('TablineTab', {ctermfg=8, ctermbg=0})
color('TablineTabCur', {ctermfg=7, ctermbg=8})
color('ModeNormal', {ctermfg=7, ctermbg=14})
color('ModeInsert', {ctermfg=7, ctermbg=1})
color('ModeCommand', {ctermfg=7, ctermbg=4})
color('ModeVisual', {ctermfg=7, ctermbg=3})
color('ModeVLine', {ctermfg=7, ctermbg=11})
color('ModeVBloc', {ctermfg=7, ctermbg=11})
color('ModeTerm', {ctermfg=7, ctermbg=2})
--
-- Plugin options
--
-- Neomake
vim.call('neomake#configure#automake', 'nwr', 750)
vim.g.neomake_warning_sign = {text='W', texthl='WarningMsg'}
vim.g.neomake_error_sign = {text='E', texthl='ErrorMsg'}
vim.g.neomake_highlight_lines = 1
vim.g.neomake_virtualtext_current_error = 0
vim.g.neomake_fortran_gfortran_args =
{'-fsyntax-only', '-Wall', '-Wextra', '-Jbuild/obj'}
-- Pandoc Markdown
autocmd({'BufNewFile', 'BufFilePre', 'BufRead'},
{pattern='*.md', command='set filetype=markdown.pandoc | TSBufDisable highlight'})
-- Git signs
gitsigns = require'gitsigns'
gitsigns.setup{signs={
add = {text='+'},
change = {text='δ'},
delete = {text='-'},
topdelete = {text=''},
changedelete = {text='~'}}
}
keymap('n', '<leader>gb', function() gitsigns.blame_line{full=true} end, {silent=true})
keymap('n', '<leader>gp', function() gitsigns.preview_hunk() end, {silent=true})
-- Tree-sitter
require'nvim-treesitter.configs'.setup{
highlight={enable=true},
indent={enable=true},
}
-- Non built-in filetypes
autocmd({'BufNewFile', 'BufRead'},
{pattern='*.nix', command='setlocal filetype=nix'})
--
-- Statusline
--
--
local frameline = require 'frameline'
local utils = frameline.utils
function mode(win, buf)
if not win.is_active then return end
local k = vim.api.nvim_get_mode().mode
local modes = {
n={'Normal', 'Normal'},
i={'Insert', 'Insert'},
R={'Replas', 'Insert'},
v={'Visual', 'Visual'},
V={'VLine', 'VLine'},
t={'Termin', 'Term'},
['']={'VBloc', 'VBloc'}
}
return utils.highlight('Mode'..modes[k][2], ' '..modes[k][1]..' ')
end
function branch(_, buf)
local head = vim.fn.FugitiveHead()
if buf.modifiable and head ~= "" then return ' '..head end
end
function filename(_, buf)
local delta = ""
if buf.modified then delta = 'Δ' end
if not buf.modifiable then delta = '' end
local fname = buf.name ~= "" and utils.filename or 'new-file'
return delta..fname
end
function neomake()
local res, msg = vim.call('neomake#statusline#LoclistCounts'), ""
if res.E then
msg = msg..utils.highlight('StatusLineErr', ' '..res.E..'E ')
end
if res.W then
msg = msg..utils.highlight('StatusLineWarn', ' '..res.W..'W ')
end
return msg
end
function readonly(_, buf)
if buf.modifiable and buf.readonly then return '' end
end
function encoding(_, buf)
return buf.fileencoding ~= "" and buf.fileencoding or nil
end
function filetype(_, buf)
return buf.filetype ~= "" and buf.filetype or 'no ft'
end
-- Tabline
frameline.setup_tabline(function()
local segments = {}
local api = vim.api
local color = '%#StatusLine#'
-- Tabs
local current = api.nvim_get_current_tabpage()
local label = ' %d %s '
for i, tab in pairs(api.nvim_list_tabpages()) do
-- tab -> active win -> active buf -> name
local active_buf = api.nvim_win_get_buf(api.nvim_tabpage_get_win(tab))
local name = api.nvim_buf_get_name(active_buf)
name = vim.fn.fnamemodify(name, ':t') -- filename only
local group = tab == current and 'TablineTabCur' or 'TablineTab'
table.insert(segments, utils.highlight(group, label:format(i, name)))
end
table.insert(segments, color)
table.insert(segments, utils.split)
-- Current date
table.insert(segments, vim.fn.strftime("%a %H:%M"))
-- Battery level
local level = io.popen('cat /sys/class/power_supply/BAT*/capacity'):read()
local symbol = ' '..utils.highlight('User2', '')..color
local battery = level and symbol..level..'%% ' or ""
table.insert(segments, battery)
return table.concat(segments)
end)
-- Statusline
frameline.setup_statusline(function()
local segments = {}
-- Left section
table.insert(segments, utils.subsection{items={mode}})
table.insert(segments, utils.subsection{
separator=' ',
items={branch, filename, readonly},
})
table.insert(segments, utils.split)
-- Right section
table.insert(segments, utils.subsection{items={neomake}})
table.insert(segments, utils.subsection{
user=2,
separator=':', stop=' ',
items={utils.line_number, utils.column_number},
})
table.insert(segments, utils.subsection{
user=1,
separator=' ',
items={utils.percent, encoding, filetype}
})
return segments
end)
'';
in
{
# nix build -f '<nixpkgs/nixos>' pkgs.neovim for testing
nixpkgs.overlays = lib.singleton (self: super: {
neovim = neovim-wrapped;
});
}

View File

@ -1,38 +1,36 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let
unstable = import <nixos-unstable> { };
in
{ {
nixpkgs.overlays = lib.singleton nixpkgs.overlays = lib.singleton
(self: super: (self: super:
{ maxwell-notify = self.callPackage ./custom/packages/maxwell-notify.nix { maxwell-notify = self.callPackage ./custom/packages/maxwell-notify.nix
{ homeserver = "https://${config.var.hostname}/_matrix/client/r0"; { homeserver = "https://${config.var.hostname}/_matrix/client/r0";
roomId = "!mKSxsQWEtUvOBTfjDU:eurofusion.eu"; roomId = "!FsUSHSNMPMVTFFcvJo:maxwell.ydns.eu";
authToken = config.secrets.passwords.matrix; authToken = config.secrets.passwords.matrix;
}; };
haskellPackages = super.haskellPackages.extend (hself: hsuper: monero = unstable.monero;
{ breve = super.haskell.lib.overrideCabal hsuper.breve element-web = unstable.element-web;
(old: { broken = false; });
});
}); });
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
# utilities # utilities
iftop curl tree neovim iftop curl ranger neovim
nix-script openssl nix-script openssl
jq ack sshfs abduco jq ack
# backup # backup
bup git bup git nfs-utils
# admin # admin
dnsutils dnsutils
matrix-synapse matrix-synapse
matrix-synapse-tools.synadm
maxwell-notify maxwell-notify
smartmontools smartmontools
# namecoin
namecoind haskellPackages.rosa
]; ];
} }

View File

@ -1,41 +1,41 @@
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
HnLfAynO1w== yaeWJh6O2TknxiBRp76MR2GnjHmkBdADwm2PsoeH/dcXsPnTftZwsg==

View File

@ -1,224 +1,224 @@
U2FsdGVkX18JRs1PA2yzT+K6rw59eH+5Sb43WTP4uDnJoR5PHfl3gxlt3mjhacAm U2FsdGVkX19b7nbPUdbUHxVPSBDBimXOIl1zpuR8ioG2AMaF2kOoOETrrJt57pkh
vLzVTA7qMMy4kom0fq//pqs5AFneUfgZRev/DxL0B3IFZz/6+BUAk9KWyEZex9Tf x+7N+/gRRTzXvEn8JBanNaY6KGIiyE2sySod0ggbx4Vs/MQzSMZpfFNTvC8W/EkN
VipKj8pQIqIVmzyn+fex/HtOVlgDWT3tEyOKRFDPaVZELZ9oxln5M4TWSbwb5YTP K1VeXluIBGP4wdN7AikEYQpJlN6RjE3VAC/oRs/QJs7peiDdCg5zmXPaz+ZwT1cX
1h1M6235Cjv0/bMI75lcEnSfVBbgY1f0KMeXxllzb5eglUBAgW0PnQ669okzNmQ+ Ol5pffkGg35NVeLxQGEIateBpHaXHy8eAJB8mpKGJKQetIX4KWkZRB9MqllpJXAY
7/ynfQenisCyrGn7lCUuH4+8QVez6A5mhWzg3spXyEQtf8hhZPmUod91oLAT/M3m nz4uhaan7zLZps5HOvyudAXE/e3BURCRn1gE3QbjlJ9RJ1uoUg7NP9v3LGnIw39h
QjUHlrJAfZS8Rth7jQmTWh19BwThTQpvJXQ6HCp65PGrx9pLUhlwia1EHYIFwzBe S5cZxD3KogqeOvkOW/49qJMh2ZbGu4ayfKP9lB3Rda1vJm95oaQ6YcaNCJm6wqk6
XUzNpwWUVpFphTPHFYg3840mAzliuqEU8x5uCF5uvKu4+Q6yEzxNn1eaFouhsf/8 JpCrotkTizI7pjhsbVD31Re0zLhuMJM0nV5HRZHMYA/UlFz+B31gzYaafpzpN+52
aQw+5lb/P7IuQ6Wjsuizw6WPRlGYiDSvfo6wHIae8HFz0+5oK2R9tZgU3T5GwAMy MNaXsMIgSUKMwKwcBWXhF8C4yS0wku0ApuA2smRcJ13Ko1H/wm1kVpiOWFaJmMWf
QmOs4EPraf7fKPTDkNzdyWGwblFbTUWbuHDYJ5T46Qzca9RjI8bxBmOkcoEwMPvc quVORVVB+6+db4APYEiXuGcvb5j9+XsCwLuF/bIyAnYih/E9pjsVuD6AQy8BAhay
bbqXXnQd2Cmk3KenfnO4gjWIsE4N/nQKkcK4NZrQVJM3ePHtF0qTW7syKYHYJLTL 1pU/9HGj49GeL4a3CsnxQ+qb090kh3p8kMM12JQ1TdjhdlmBa2YwtHhoZ8Nd03Rx
lP3R2d0omT3YwB+wvNuftqG+bn4pd4Kc5/tREYX3t+rNtR2Cy7Xq87Tc98Bj4wJL 98Na/oq7zGaFupcPnp7XvSlUWHmEKe8r/cheOoc6/JHi5rmELgYQTdEv7y46d//w
arwF+ND2KrmbzqSQa56p4Tc6Cb0mUf+a8Wh/lCxA+xv8h2Unj7mQryjg5G/wFq9T GjcuEmI3wVqEwajJ9QoUdXluX7mEjIk8S8YfL31WitBEvbl4gf0rfgYkbctWALgc
mPQGy8z06jJtFRebKIxkvnoCFbA/23bxz6AWJ4hErTvUYBlAfiJkfVwl0P9TyhKj eNDij+dndWMtUDEtrsxPtbtv8puLkpyd62TBUNdLAlD/LFMgn2B1YR+P94QY6sS1
yUZytY/MtUzS5tV2ZhcUjd2R6vefl2sJa/SfDDrHyVTZL19w0LXH+9fBlx8MNvk1 9aYP/4VKryDPrEEZdF8ykvrHQdG7tyMkEovrMg8Mlxmkp9dBVT3S4AFOtdc80BwK
mVeK26/b2yWExMlSxKOq+S7mtYvXp3vsAi2LfPuAy3oD1F0hcFg81xABpxWMAu2o za+5yPmjNNkodStTRlmtLemJgDeY6rtb3jPVlekFap48fLU/kqQlUUm9WXly4CcD
8h9unJAB1JNRkJPHLLQlXuRU/sh/FgvrI4OPVH+UI+XE4YWXAZPKwgbn6eeBWRdK uYTL+L1VwxwY6ZFzyQXKXWVAH2jGr/7BhBTa2gFpG3QcsWJUPFTLBd8fb7WU4SQz
aF6oe9bq0AJoPr8r/6yGJYyRLfNk0fhmJTA6Mwa05baZzYfFZcVJVPrJ8mOj2Sw9 N60KzFwNa7OLvaUiW3RKH09BoKs9I/mwqbRo5GVE9Xi/01/IymE+vS95FILPM9q2
dS1pU4++qWlWVyBcVM9OzveOX8t2FvfN4nVyHs6Ehjses5P7skITlENUvvYEbLo8 olyzgoufWlm+2Mv+l5kITH4LUbFK8+65kLnsmyaRCVqBGtdmsi13c8rdSQtPF3xS
/xCDQSIN6zX1Ji2cWyxtok9qkXNFN9fh9wYYnO4doSytU2qC58zaZGZhSBmyRS73 HE7mDw+JktNSTiyQbCAgXMuDd/zMipIi/aylmF9jZD4BYF5pSnQFn/Rqf0lCIySG
vfiiky8W1uRVaonDL9fDUdVRTHcuMC2HVWJh5zF6Bn3BheKH3jXNNP7zDoCFabt2 i85QsjZjVX1veW/6LWW210vMlNZcG0u2XWM2zWIvEUV/aqeVY2uRb2/CyLBGA62I
WU9BZXIIPtlxgN32RQjaRNPTgqqmknY04YDBmH1QSWdtMlhuUXgw70z2Y3oiU1aO ZejZa+Mm73mw8gCWAIB1v2QbCKpGM/DqzyCAX/zMx+g8Kxml48PPzL+VQ/dlFo67
1US8LL3uQ5xMaTXwNgalVQKa0Vt1mcD2JoyMZJ1DnhYUmXZA5eGX7EG6f0edMsY7 A9oh8uCCyw1D7bJUyRvSuzcLPjJ7BnVf4qEaE1e2DzXVKkcaoYdrYJ7soJVSrMKm
/DlPSqXH2iYM0Yk986ehIK6yDP+aaT8j+uO2GhTXp+D6d++H/IW7d+G8hRQMoRRY NCCDevI6jCQJZ2mTr7r201z6rrvukRhvMa3ByMa1ujR9Iu8n+EAaZP294oT5/SQs
tqePEJRRa2BbK4l4czhfqj+JQ7UZc/3i2pIKkzCUpF6Wc/YD5QZFBqLDMT3wKJpy /ZLZUgj21+7DtT2bcsmzJM5oTbbht3nJZYbHA16wDdyGWbSmV7erAsdaZXw4gqIl
fIUdH6qU9VFu0kBQU6Mvr1xtrTSRyB2/+PIX74YQ7GHeT56OLQkwAhFvzTyiMLgw 6gG9aSQxBjH1L+kyq0rmezrP5S2GUpjvrV0o5zv9yy2BkbOYtQUNuhUoXDHHK++j
upYF6pek/ZhC9HLdeEb0aBXWREJx5pMimbQWPI8cJzrl1Xmwmn+okHkrpHiOkY96 hR6xp5E5SabmcZmizVqKInqYfhKRrfEBqW5CRdOidjnWtEAzDVz9EZcQ1Vmw4EiH
SwuMy0QrvzQpUVGMSTHS6rhos3EelZW/ZFWd7n4VaZ79AR0VIO3hPCRQrUO0eln7 9Va1EC12cAn6HfFcxaz3pc4PUFxRWZm/uxOceAokZvjsrWfiT/ESif0iZQNfEJRU
fOaztKHqXPLvSahHh4asxDfhq0BXLvqInIbdjigl+hzRGfgMF5g8EouSi5KQ48Fo 0kfQamVQVAFMAy6hSYXINBDRAdleEBVzkljgTR6tA+wYc1Xy85y/ReTfdTc9viph
WN005gy3Ondg0tTmVitCqMBNNJtN6PbP61s5Afa1O8fhWIK/xtitxCVlui5A8j7/ IpxiPTmK4re1dLo1L4rZoznw35qCtTXytwvaZvNNK7i3nGnD0Lz0eWgI4WjtRCo0
WaRa/+uvlCOBgS4TtuuhEPLmRUy+4CmQtZkU7mMl55zetzYRRcut8oEjXmKfC/FF p1Y7fXNc0AboWxBcsppNSlc6WbFbN91h5iTuvAcUuKbSL6xROWcjzhe45LJ0nBlK
fpBOKSAVlfJWX+nZQa5obyAmhIPlIwcH/zn4RfFOFfbGUKGbHOu+ctL+KURqsH7s LMg/1rQb4dKGL8BllmTpfI6xqNTBkRyHkBeebnzHmlc2nMoKPlYhAlbrEtWq0Auy
/hu6msDRQ9ms/i/7xacsAPhB+ASyzChYHeoSXqJOpJLmJfBRUWmcbpyFuBhcxtIK f389M6A0x0lmVnITexTUhARz+xj3gTqTTZN9GcD77mtstHpoyyt5yJIIAZRGRSyn
xE78xStMnIbkGo+NDdOTQqNm7kO+qUUdD8Z7Kt6arPwt2yucL1eexGtMMK0FCNBI j5M+N9fLR9y1l4g54pu2AoG9DViwg2qyJunkRMQqQH/VL0ckDAskZyEdS5tqSFzy
82iPG+nwniyiP1z7mUfly2/FMtCJOUeJ1or7myNyB7oeBUGAZ8oxCpcPW/FeBL30 upDVMqr1fJgg/OUpt6Evrv63qx665wtgevMLdvrT8PGsb6//3xV6aCYa0UU+ifmw
Pge35VTST/EwLEaTM0QGnE/E/pawuNznUzY20fldarlVqKQILhn21GKaGN3AAykt x8YaYzXC48F0b8fo17zyHWNQnhloq2eRinsHa/kr4ktgNbioBcY7+pSmcawIqT5N
0hy4YgGGU7Cky1AQGLYzq77/9dVfZh2RBCHXg9O5w1wkGL2b0dod9oeXnjpKPJwz 5kwX9aO1C1mY4yimjqKmiO7ipJ/l/zKpeXbjz/5Ur68hgW/57g1w7qT5AXDHzrR8
aZ2rt2FlU1X2Z9HfUrtwg5ECLkKb/JrBJtWm/YwUB9rBO+Gp6fEkbCdMlK48X2hE TMi6DRauN35Sa1aHa9DbVL+JK8lvReuPbSDm//Zcm4rggTFPyPoN6C9eYVvVwjEs
MREfRJ4Bf0OpYngCefk5jbOeclbB2hog7Lvt6EofWh2Qg5IULrziscfVaVPc37ZB Jrf9/SOOUiTlhdMHKD56ae0LchUS5cfGMCvRWvqt/wiaGnTd8eShwSGbtkMXnQ7g
dg+wPWaaO4nCUNYNks8XUVb5jnqwNHPVna6vun7DDiOJXmRyT5UtDQri/O5/rzZM Utvrj+fY2gypDDiWYwRvHcdedGiOl12Ds3XFmv0NhYVCwAbaejtO9mbI0E/QEY37
mhrl7s5ZNUw+IbwO/S6wAH1sZz5fO6os6//U8A2WZWux2XQgXLwtZtHSeH68d8U9 r1HztiCgHOwVPNUETRplTbbdPfByCNbErM1kt2Iw+dk+eEMnmIs4Gyiy8rihHb6+
MUgzL3WiPqfdrvEufrJHjTDC3Dk2qA0PHTClkBtzqd+VnY11e9L/FGsRipTC/95u IXXepGhQAIJ8EGWfV4wsum34bw3ugzSsSz5criVj9S60Zm3QaNNqcWmShX+pL0SF
QTE1f7pqSeABHk0fIHvi8d5qLLeF39H0iKyTWZ4kOOrxniys2ZPzWLW3qaB/qTLO 18sxGH/FDDJt7JqURWqSp+N+VBlCWx/Tg8X6i6J4yvdNc2w+QemheqVRawOJ88JT
PYPfE2A6dLNO006G2ITkp3QYZ/tJHUi1yZC00hjnG5Yc/5N9eLhR5MuTJ2LLG+VX Nbmn0jJ82ntKm7glPqPdG+v7aYCxk1wfzotTmMc52opgkd40kDGTgCSbk6zJVSyh
HWf3qJd/i8ajhShjCwp2lx+t5qhOl8Q+AGPtXSu5pgnb+U/G55UEE/fjYR2YzETD sHsxya8woK20020etxBjp8OO4sYrZO4ou/EK2DFU2jS+9Per/wTRnFWeBvifrqfb
PtgyeaKQlw3f64vNkbSrt9M1MiI4UReOuwVEOk8BRghmNTNtqTBLUP/VvuRDhur0 z5qD4BjQhaNWJUUDo6NCJoCOXzz1A/8RDp4BLV4xdnfQkLUD3hXSp52FkVGDqxPk
mRgMGGogvzk75YWnzsPCPNj2Qxi5/y4VHAOxyAR/W1npFMxNLHKIWxlkYxzt9qU/ 8yKt9bfoNYIAV73XIfDFTLrCMNqGq+PO4qBq/iE25izqD4U1sK4a1A9Rv0E5zgyf
tw8vSkiE67Buw8TdTx+mL0jPgvSsqGKrgH0Gi5WE5UO/2QIRJbHR8gKoG6AtuHki +/MJSRzQkinCVzWYh5sLvqv/jvfQNcpkA59epET4CUk1Hg/VynRra2rptayPo3sH
D0InLBSczjFaOV7UhAmuxcqfqkDewWhyd3YE8FkKNgwtgTM2R5KzmSORo2/koEmQ eoh5CsPyvOq87V992f1s3tWxD+o+Wz9t2U0FFL4q5RsXDHZ9S08nowTIqo1UnycR
lxjf61lkSuFp79r1yKt0NVYfhqSgOvpF80BOS5DiKezxV+dRfa3fXlxF7L87V6fL KIIZSC9zE4ab7ht21OkyEmM03jMBuoK+mIC+84pIHQuO4YhVz3IYsIZ6ZYSZQ6T/
61JhyorlhN5lzJ6MamWnePkndxheL5MvdPOWtHpHZXMhumrvCA2C5J1aFultbTz5 Im1Vfl3zxnMbG+b8BsGweyvMP1bwDdpW5FIBdAqwNxQ0fAYIGfZN7X8h1wh/hUH+
ZNuSDSssEVMYkIMtJhUzgNTItPGxkGdCJEwzy9IOwdh4JyFawFuZEWYER5/+xvAl Y8SqHtpMVLxzpEkMlSP3RKP+nUmtLaFihzhpJplp+b7qA+CrvF9yG3hBD8TpIUMa
RjwoSc2i0foqdrOIT02/69oNle2dVjWDva3hOxs9iCwwYrLo+crSm4CoAQ4me0Lj +USFhhs1D6SJSu5i5oAxuTzhBypxODr1UBsZI4J0SQxtueLKA8hIScNngQlIPrAz
Q7ZcOz8lhpmkEayJr4YWmYf02VMhoEWxV8KvPPFRhJb3g2W6mJOOaIjrB4g9Ml74 wAUnMlrsqyItYy8kj1/bRtAsydbQYkwzIQAnnfT+S2++W2wx/NPx8HKAleUQapJa
HuND1rg7GDbYVjDxHsKSzuF0iw479oSLe86yinRlUSzyFxcuoRhvZcb624N8rz5m R/L6tC883v4xKAihlDSMytxXxuHkkuucrhcHL/zlXmPaINAjVViPFuO/UTedKWpp
zuLS8encEWwtdZ+yitHnnwr7QI2AFtngpmbBHfh+R5eHQ83dbUjUQnYgoRBEzqcp FE/MGii0tWkHUMYIz4fNbHSpBokAu0yGOvDFitm+eam0qSJozoBYKYCfu0iDaFNI
XKtqzfv5JvBV73Ruy9ut35p7sTFSSwRSq+aQNJSfJQV2kfmNGoYHH3ajpojYR8U5 JU+EA5yCGxQRhaAT/JLQ729HNB41bNUI8udrxU6ciWt9g9eLDCqXMa75JDzpX5E4
COMAjtUtGP3wyayY+st6+ktIlUzOPHcQtOajmhaGQCd1OLMwxHZ8jheiGumAeFNs ltoI/rnA2JXY/WXBbkNbiT6hcRzQnb9i6/80aRrZgk9KesYp4lrJtKcAG1ZHYJux
e2oHqYdotgFzXKOIRlXlsYmOuXL1385Ma8chDMxwXUrvCJjPwKRmAM0W0iHf93rE +0fcmGrQyOU+F9pFqd5nEK7khS/fUztuBRwESxpOVk/0shBMyA2fAK/e4E7dG+uu
iLej4cHK6bXYTNjWYokzAhEI8EHnbhgZk+Ver+xUC+MueO4iGpXWStHcymeAMGtS nAyxKuHLOcTdtjh7niGW5w7atT6nZOCtBTQ8UpIuQKOwZV9m6fhD4ugmY3B/BrI3
c7QAnp5SCqlrI+x0nH7qS4RcxPK2quQ7ELRJk4I4RaOpNyZhb2LfV4EcZfZtQaBY k1ve5bP/fhMv8LWl0Ji2yCqtqV0uK7JEKq2EAops51xqsshJDJg9lT1tczPjy3x1
+bRTYi/iYJVmwTjlT6AeH8ZLQpdfa+2at/HGm3ssXksgqQotNLT2bHgbPTXTGI+m 4EUZIUkJ2pSYHGxUoc0LGWAYRBeaSMVqiWWOBdWkK7/Gcz25b5P8TIly+111zVCf
dLBLqdBjY8WsT5uYDf0wCUo3vGqn5UDiluuMbl5giDmL2okL1Q2y1Vt6RsZmU9B9 RIqb2eQfBOZy1EuRGhBx6Q+2aD1ZPYh7Erp9vLKxraf4Z4ojh0Afh/ERyWiq7b2Y
rxCzik0RaEfo0+zauXKDwMdR0JRvOdTSBSkahiNtrrKoDXXnIjzLVGNgdD5QN/NG UDgdEDKoWwygZSurlcytOKzldTnALBD1T+T+FORmn5k1olv3Dhdny5ufGk8bsc/g
L3/r7VUgi8qZmP/f8VtXVE3Jvbt0EYoe995OkZpK4wgSaM9D1Dob0QOOZeAhfU7K wTwY/qCXgwFCzznmk6TmPh527W7q0VIFGpMfMV8jzkTg6MsPZO9ljlkG4t/VoUuZ
DY59yFxOAQyAtBb4SpQ3MADnGuFcknePw0FdQvohzDWhjqXseXVXP+kdcVErDHek dgtd/OtO/JOOJo5pHTHvy8X7u29BKfdm1+mu3/CF/jUD07XKVV5UboVLXgYeVLd3
fXXoFGEswusg1Slwb5SaH3mLYqjyEF3DvVvGrzsaYpbuHN11vt8y9rPJDKHl7E5h tK1F3BbKm79fJ4m6eWGYtPsOUxNQFM9V2+2VHphYxefVHbuBas58qIrPAlPFFcWc
o+cZjjg4p2lOprZOV0OXC9bSB5Rqbyd2d8P5w2ZkFmlp9icy6lz6xk3MDnZ2Z9PI sq/QQwQtc7f+LnDSNjc0/ttkFQtBV+zrckc3VQXThGAZ4Dp+zcPvlmfCvHKi5iWx
8fRWkADf9IZSJlIeW5+b5wt40R8QC7gSLpQSbjB3ZFlkiKCvph6M4Rw7VR7kUieH S0hqDehktd4BWpCcgBgiUL33naSZ5TFeXI/9MeQn1d6xIeqL+D78Gyu66fzEJ6ZZ
2kiiIAS9V+zuLxoiVu24gPGZ97ISyF/Jb8w59jr6sjbM42clgGQrXP55Kq3BgDAh CisHgo5RcS6nbJAm/I0bDeVJ8K0JHvrqZqqSR2TT++Fns2bniV6d+blFJ/eyKlXi
4emOO/PZjRRGS4dSMkngv73DoxBN5uP2lUGzUHdSjlquTLG/OjVHCsPnRY0MaG2q kyE/sZQ2qjdve7HCiZiRVKcWGvz1ba2yKX9hEYObrabydc0o2Nn3pewBmOlD0xFq
yolppl1xZuXT4SbIS6cFMVAuGGYLCdgAVxOzKpzWvQXMJzvkTt2dciVl9LoYuvIX r3clZRREj+J+YdfUXIF7rf83q8RoZVfXNToTIIsbhRrgizFK75nCrL9wX5GXAC1O
AcKsSyX3IRT0Cwm9OUHIpkT90szlF4hQLHdAbkohmTP9WvqctLp+VNcCwnX6SQq+ eSs4LOH8p/CTcGaXR23BgB3L5uKlfnTetpjnWQtpVc+XXep8Ni/F36xeC1wbF+xo
p71EJlgtD1wfYGnzsl747lf0IRD1dtywciQKlnbqd0F5JzJMSCdMQhkgeoU+QvTA E8mFlm7i2h95D6UdTsi7dJyJf5iAp40g/fZqMc7Thb+i0WD4HluPqZHZ4mOmpfwW
tK8L6U0835+Q/ZAaQixrJqM/tozoIt5rL8VLRK1JQNDpSnpuD3KHivRaCBAk9B85 tYAbxFyih/UNyT1C6bcA8+u6Hnb83rF6yGo5x1UxZ+6sQU+DZX/FygEiLpPsFxfC
m/GZst6jPHNglED1hLgGk1KTlASNqwXgSyWWgPrDcx5C8UvlVptf5EYM+QUs5egZ 6NWLPaIXYugZCgT+zBr6kKtJ1HVWdqhLsQoxjmJv8rnZ2+pPGSttmfKMfmt5Mh1T
ZsPFVTsxSs5pk1IJaFk0FCV397QF7Wr24QBTe/0Fa7SvaXDbtyN3ucgxGwTCto+M z2So5IWSsuM65FfTrZjvhPZUBUvnCWA2/HwNWxqkJquX//QX041KeFAlmk+mcVt/
4G1LZ6/mHVOAZBZwfws5G8gziwfdueRivZsZOClChhv1/DnMLwpNlayGPUvkTrAS mO+V3j+gk+apkVRY6899W0ghWSY/tBQSCJEPoehhS2Zs9hcRarPDE27WBh9C8IrV
Y3riGIwKG4rhZgwuQBxIRVnlHVexatdCWMjcz86PlgL56cQ4v29pmbATQvYcBgiu RsNGG19HdeS9WSfNvQluro7PsOPOdK3BT+j8cbcNhNoAtVFt4r9l8tlkwTPY8pJ6
ulto+v3CqXNJ285yBxSD3BbjbvcAvFGOPDVwsRzNan/6bbVRa87Ho+61eJ6Ptpe0 mXUFBqQxTrr0hmxzMh8R/tkmwTMWfTg8nXRi7X/8dLiyySBXj996of5265yKwUEI
3Pfde7ynHy2CHpHs+jA/AGVwurfVitn2omT2f07JZBqtt61CU/USOfx+sJP72Jr8 yLPiiVPVk4VO1jL2w6zNu+VhLMTIBtDfATF4D3SQ19kUa/lVuKUMIMAIt/rUrja0
f25jN/5mjoNjeIYFY5Ya322qQQsdb5j9pL5bKOpgLcKX9m+6DhXWFs3yRSvOVWqr gz8QWzQO/I6MSoLR/B9JSHvzwv069UQXFStT3yCnOnsPnVlB7CcMTYNYi9q8TvVv
4V1xTRju6nAs1o+7za8agThCqWmxUy0D8L/7xleuAFhEFtI0Gr6faPQK1AAFNAgM VMqm2qXWaezx6Yhv3CV+o0e7Rijm1ghNwG1hJQjaJWBpFTLTJmdvzpcRXdXalKUX
wECwwADlD+kENypKp8UOO8SPGLzvFiBhgHTcETpH7XgzxSwPBb1YHcvGOq+xFHip 1C6LgVDq3Y1Ws7EKc+c+QEqp9RB2xOC0cEYbpr/0awqaIATE2N/3KZ5xRhb+i9Id
tv7H16TpJqZYhEdiCr0BA0xkiUqKkLZ1nVIY9cDRW5W7PKCSk8LJ7SJkK1axTfYU 2bNcOuylb/4Pb+6x5MnCkK/Z6tNegJwkjTlnkl8AlCBwkl5PXxFwZIa/6fjWFT4d
8EBeIX6kz77aPyxOvar1E/f7mF3jdUDLJZq03CWrHLI+Q74wvHC0l3FRknGc4fRs a1usY6D80FF66vTO9X6Pc3QuAe4OJLGE9mgxqGDphWuLbc6k7P4HSZ+TEf4uZDIF
ge0A8Z2QN237JcWf4RDQGr8hz1b6ynbg/lzHTQtX4KcJfwSXdxx6LryOx160jfkc 6o/4a0FsM7wLza76IIQoSdWFKBb0Zjm2G+S6HzKZjAMGW+fuSR5S+mKrYt9cEWl2
ugvnaqzWxDWRF5nEvWo+wwg/ImgBe6jkPeLNUwqfwOHG1qtLbofCblr2NxnRrVQ2 tNmPfAVagYWxVkMOYaaDu3Nt66Z1UAOaSM85X8WSI0v1ITj36aeSa6TSTvrDL7og
FmBSQhQpxR0QoMd9Z//HkV+4KXjmPxX5yICv1Pste0YW65bWoUiUtLuszvlpleXG mTNdkqKb6L++xJohJhPcSR4D47YwGhjEyjeUhUjry4XnlPsC7xMy6BGw9vps1xm0
phtnV8XWYAoX6p57iUIA3CLDv+9P+cNxEMTKohmow8yaF/Wq+n0wlB7iz5Q2Kga2 lOxkQmJFNkG4/dcHtMjtTc81YNb6u4iumoypzEgt3G9g29wPP7NN3GEHHyW5xIfU
gi1faTMajOBAKD6YWbRgju1idqYOSaJBEWRm2eEyz3/V1/oDxtPVl0FJEIlGeG97 QKvAOnGDiOb3X4cL5U6h0Q959Zg1nO+uM/pY9Lqh/aXtMKMpJYNVr4Grv9mRaQOP
2fc4QVGFjcth6K9VAowstV9PAo0nJTjaV/qHxZE6jVK7CqJoc2eG1h+qsPAhz9iY 5Gi3/yWfem57RCclo0wnCltvYu4k1fbEpVpEpfaG503SJlUGlG9ZhTraqqk/emRc
LAFtV0MmXII9UEnL0FFOn29Z+rHYCrXPAD//f01YQsLGghX7VNu/mLl3w2+bfuXt ZHw7Xj1y7ePB9Moo3/pRC1/SvcH2ISXbV9uXSZ8BPcvemXuqCXSqliNUSH5kNz/C
EdLei9zMTpVsY/CTdvzAUYNp5zRt60q2izaZGXKMgFqGMiA24+MQJYA/g0T/27Qv wA16chg7Bcp0nble3ZQ52YrLoNxOthmSS1g9jqf9SuuVDVpBFNpPyTJFeTb549I+
bFCI9reXyMsW0rTU2oQQIm6BLX2dPdIQssG/gleaiV8ua8o+t4ziPVx2QCfNc4j8 DvWZ6amA0bpNIIvp8p1ALvbqXK0pG50bpwkkQ/+Sz1VRrGS3pKnq+oDo5miiXRG0
2MSOy8haqNs/dX9+3uZShPK+uC17jGURYdMtF18je30aL6dmD9udsUmwFeje/kir kIdnSt5fErCs99ALm6CoHyy1ui2Itom466olwpYfw69IV1Gv7CwfjOrxT0YJ5neY
Qw+qO5HwZiDMHA0bu3gzjDP3eIBOey7CbaPYHkMhGIOp5qzaauUgB409W0226tjc xTfNjgLHa5KmX7n9U+bOKKU6Oqo886VQpyx87XT2kfBJVA2A2jsFujuZkQwiZluF
vGy8e+J2w6ujtLSr3kvQdeAnVECbH6ajVfRdqsVxT+eUFNJ9BXyY7cZnGj/bAz1Z OKBCZM/EvSD4hvmGULW9OXCILsC5qEZF4qlXn7SkL0xN88cTxPGwLKBBBcRrH0d/
nbQFKWW1tHTgQHQG4ZOKBbG+8wA/C9WTfd6ec40uc75PeBHHOJOpS+6KsFfb1IxW lLfiRt54oIWv4llHyNQv8GnWfILfvYbPt22ygu7GoGbqzXpuNaSozzLqTQPD/3gO
E0efqSLb3t050aYWbCxPM3cN/JYVCfCdcr1mWebR7Tt5nRwT7gxRj0Tk+0/l/OXd BBL2p1bJbUtlNsvKrrjg/8w+zBSvJQXIvn+Au5rUBmnxid6dUIe4ByAzH0TGFJcV
LMB0gL70RCDIZBd70yOLMWX3/nHkQxmDKbEg7m1eGKWtRC1CAXCa2Ej9dNbNCdyC wwU7YM6D1TVQQBjNguo5NytT00S6gQSi199f7E2KMIMNqGgnlLOjDcnSwo990PF+
iHAZA1PnPcuqDhT/Du1iUTlq8YUeKFjbiyEQUB1G23Okg1V00eMWzg0uZnvfN9qo qtGqfyiCpEUJvbZF9X6OGXuv7jtYrUwpBpoaELTd16t5BHONrQ1PGm9R9vnKk+Mo
tZxJohsSZq/tgOLvDn6qJZ9he5ZWPuQff7NFWKND+BLzwdciKq5YJTgXL97ao/4/ B5THoIeVAasdjd42p9RYMkcP1X/xCKnIZlYPED87D9oypOg0kUu68EF4bd5DP+X2
WxIUBoHrX3IMAuFPVE+1d8LszgmOEwoS4j6fA2lHG2Uri700tM0Vh3qOIkmPSiey zHMMXPnW+e1K0c5iUpzXmzL+Gs81govs5nQklR9yYRIpahzoeve6j+kz0r85ZMDt
eCI+eMdCMxWnXJ1Xs2FVDsn1+qNgKTooLsrQ1CDnV/lBp86oN7VQ8UufpfuMuv5T mVEXVb/By8Sklt2SrVjZ//10tl6wiR5wq8r98tzkOLQTn3y4J1QdQD9l7RxjPaM6
kJFUAFYkLqenh98BHQIK4Ef8VnlEE5V2+gsZgGZc0fsv9Pk+G2fTSi4sGTaUbXcK FYjGP/hG1CkVSEkQeC2DEoolRwjQv7chOW7PAA6Fyl2m2v/GmLMCYmrP8PZK9mLN
RCcYAI/YCtOfM394vzoePAtT7hKnoY7uQa28S5zzAYcIZygLWbOmRZI5uZ8LhDdr CbjqS4RCbxu2jHBSPjNqtP5gH3u6tyRq00sn6KS9Rj6/sGQHjEV2Xb1CllQEmaDn
OBKY6DOGGR0UzklAYus6ibFL7n1rh6OdtnavdsfjIm4JLoraznuAs7WjyE+MAH8e CuqEiuQ2fiDzjAlHNUDwIW4ind7kI5StqXLlwG1elNLzM+7ycUHhymwU2eu9I6zE
CU+alwyCgtf03HaDdw//QECCzbeJ5CduIgqaLWgTlnGp/XfzbK/I4Ak+6ILie4jQ sHjzU3AJc5xmLITdJEjqDHkv3GZ506RqnRyvKGAmUhpDmEyJHU4gZ+SxYtGQ28gl
c/lApBAb31qJjtZfo4PNpHqSkGywyUNlbGGz+AM1dwtRXFqxqBhGykUduD/qTQ4m Jt/ALqlJkpEYlo884HbG/qPqPebXGoRmGGZyHviMA5MlZlDlrCzwsEno+/VPNaYc
Q1VXar/vkmy8+2Cvc2kbMDOw+lkBPDRhXtxfGU3sa1NFRceo3VXH6VwGCT4gfgmC 7BC0ZFAQpQ9ZKIM8xiaasl0zxPPHoyG9PwLccMqI50RtNSZFlEVUBvC0VPp6lje9
Kd3QWKI3w8p+i3Wwlhzt/RoArSihQzP3baRNN8/66KXFpQjcSxU3SWdCZheiTkbr lex4DfrAkcuLr7nqKw8/j2SGg79gKihmX2q+n1hZc/BX5ECjaoxSvEUnPIniR62n
IQZK238ogpAJ20kjOsQiDshIdb46LYGe6WQ3JkCvUwQu87WrLigdEi7FMdgNOiyh 2AsYlaD8N8c7Ylq/XBYYbhbGi8hDD1S0M0108NRizwGHBMZBuhQe16J0doqrHYBG
bVUsyy9i/S9RVM8QH+8C/svWpnBvJ+vs50aTa+hxVO+OtnlJj3Rpr68or00Sw6cl PExNdYcz7WD4EUnepvRaPTsiX3RkeXBucp5MlG1fUJl8rD1W8ar/KNIa1ARIugrk
aJP+mY3xhU6b+Kn5/N9VXCZJ1IEjUGEJVhvGFyUKFIDo1ASZW5rKrBCv0NjKy5do TlVeH9PeXzZk/bRzIel4Ue7WgOPoQ37ukiSJKCFUHRMi9p5cOOY5EIBc3WhV+wzN
LNCLG5ar5l3ZihPFK4jUIJ01SWDIToaBmpUWFwlvfLDW/zpoX7SA0pwITi0nyWSL wc4mcM1LEimDCHGOKt72nAD8gKmtxinnD7b14gV41d8DTgmK9dXApkCuX+7SiHEa
YjRdqGJxMHWHsC8Qdk04VPN3OCWTLRUUpRJxascP1nCSrZz0NCbTvz/l2YGYe9Zp Uc6rLnsLWdseWxUOgr2m0YcA3Jy7dtzn1+0Rw1CAHFYjlMIPdwmmGUnvWEam4WMo
/csottAQ9gwHgBxlSN/3aSK/dC94PpeEo5nliwT1qU5yq6mqwze7/juFXVd2UKIM XhsxmQHPPRPEpRW1587hOjxME7aMGgWanB+wDxBJpzoIWW1DxxRVhhXOsmaKlW9U
7Cu/oBAUA5raor1zrUHzaPQvhYz2IwckFf1IfPs5IeSOwrxjtS9O6nFYVVHM1juI M5LzOhsn6UG5AGLubc4AwUcAZLKT6dArVLmxhgKpxNkzEqLlgCsSTSXjPo0uno4W
ljVJtpUi4iiKOv7ybVchO/0NnWS6ImbN0+V/kcl6GJIK2n5W5s4DIBBqEtkw46oK BEBv0idohf4xHJ6McdTtThMNdudJ6YVo02LkjddTigLiKOZm/ad4+mzTcXoypeud
vhPcg5ixsGG1TSsq+c3eAA9ZKOk7JMkYfYZW3kObo6BSdKpqqo5PJXafdRf7Z0UI gN0UCdsrssRAp8ivepFFhZlGkmd+skq1+slLU0f8Fd5D7U/lIWoq3bR3eU+X/LAj
NOFJ7DMq4vGqmpsl12KcvJcpErSMc6dELlz8Wr7+hzYH4QnzPaaRuadAmrFNbKmU bmSr8/AHFnPzNy+xYXOK3ulUURiDqPzLSddE/0EEKxe6eDbSKEGn9L1zQHaKdiy+
vKA97guof82tz6LIicnRIKcb/mrZLSQiNpBom5q7DjsqnqDXGQZbxVqjrtySfdvS JmcqD7dRKX3txuFCnCKB4SAOJi8TCiWyjaTpm0gdlt+x1vSfZ4Xx3zx8BXRLpuMS
cnMNBV6oc4Sdy6nq+ewdrh3JbAL3zfrGYsAhUgNk6OixKhs299S5Z0uoyJ4sBrUj vq49h4m+3Czs6OKCZhwNvMnun0aaBtj4dGx/haUojpUdxjVz8s2KUE4cTnwcC3vQ
a6LlGPz+/kyY9NkIHixd4h35HMgBwqXkNYh7X5oIVpuT6rvtdR+wZZ1fMYbbC3R8 2M4B3mVO45aTcdcsAq6IRWsJ4CwW810FtqUSeWgECYc9EqqVqYQG6zcCc2BzYNB1
gUxBG12yUXE2gNeyb5G17LWiUaedFl2Ywu5cVL8wMRUQxsJ9XlHrNvrxdrw5maB2 iQNHcS1fhzJxQ5YflW04OtioiiOSQ4SYjESeTphaup9ZmrJNM0/CifM2uy9hD99U
Jk4ieA7iPtCIZrCLc6KImZH+4QuLIY+wdy0x+JEc4G2hJGUWvX0pzWOHl/iJ8k9T fww/Od5GQWi/8rCXc0FXZv6GGZ+Zt52D725o6FtFqzQYthysfbw1hrzhLYsp9vU4
qHuUUbLmkxmSQuMx/uk6Te4OzLtsrNcHKFZMbQpuWTT/yw47IB7GxVKVRAPYxEea WF7QV6J/zl2b/8RgmDEN5wtMf0OmgrV7znPybO2k8/IoeRQo98O6Q3ilyCiKPsgf
06OytBcLZkNL4dzKohOpw7Irp6ATfUKRLzI6WEG0Qui6DYkPz+I4LCaRoOZxgXx4 9Ny0VJdE6abGjVqeXb5Sm11o6gcnCiayAojHWBt6vIxQy7wGyIdsG7dJedmFZP4W
5S4TVp6HZTKtCw+rqXxL3OUY0nWLsOioomJNfIAXYZlLiljYBmNbc4CrnyaxnyoE om3T5bFdOmr7RoXj5BmreoJQ+ZuATDbPJ2ZeKTah1EmojM9xmHGiyT4HFzLxIKw7
9FEno1GCxYy5IqVUWKzOrenR8A6TzI8t19CoisXdKgMyhxgTrBCVINfdXPDZjiUF Juhwb77oNbEzIBRn9qF4q+x0q5y3itdj4gHBQQwqURs0dt3ODhGRH9RqzRohheqV
xoWkFg/09BQmb6Yy7Lg27DlxSiCtuOIvEp1vYv5Bp5+FKhqaUWMsRMnwzI5myQ40 2A+oy1Uj+2LI4XUnWBUX1UQBoEVTa0k6pQvJYcw54ltPFWFsmwgF0AXYthmJtbpN
1cz2AeA/s6BMa7CH8iV1lXsUJbBho2aQp52c2TcUCEgzg5ZDniMIhTu5x9r+nabv L/WgVPRbIrnzyL8SJf0M0Im5Ja8SMwehq2Xc2gmqfaZO7IkHXDJVzkv0QZtdEjRe
l5pXOHots0jAlKi0J++VdmABRZ8FYewKsG/xn9MCviUcwaJH5tAlp0ZURddnTzDa /MRfXkGoze+Yc3XNzzB4PP69QeMSwNgO3axVF3KwqV3Gatkjpu471QKYECS8DY1B
lTnKN6ryi8wPxbwtbliEGMmsjteMku8HpjMaM4KuF+gOZD2HfRuer3dfNGdLeebt 5YrPbt1OtHwKgA6J+Ax3FrQ9bjQevMQcp2AZ3ig1iKj4O4z79nzA81i/ElHBkvhm
9rfQtAEVf1ZWJB+GobDGeJFKzHR1ewPF2ULSnJ4rOE7cwKbKB75J+SE2w363z0vY /J7ohj7tgdWIkn/8uR/v/i7II9gUcffidEzsib1WVkAxmd5UFAOSTC8ZJJZnWGKf
caiHrV8hTL21M5HhlTsPqVnuV5I4k6/geGB4fagIVXFGRdq+xRviEPRb+yTLsuJo Fb1dweJoPJX7S+6TuyIJqEOoaOu9rFgmBg5j9htB5cwFTf2OsLCR0ESwTwDwrt5y
IXGRxycLTgpBbZ+QFd8dl3huDQ/sB5vDB28kZzrl2qObOj2l1LfVHto7ven0DYcI rnouUQpEbTvJ+DKj9UDTHoAKQomn2T9ZhQ4Bzk9kIcqTVtWDTOgJjcWVhbNdLHKE
t56NB4X4sdlqkBNabzwpWDAvHaCXkiBZAj5fvsdy1ZqJrxUeArGv27FLg6lmwv7p YCcHaRpav+4Batxuy90kAcBWk3xQqR9/+SOjh3v+Y94D7pbynegJHWci2r6DWQuI
APsEVtUT5eyak6+DEa5ACaDQlArl/q/mzJwYnlCURffUDerVHou+DNffUCrsgiw0 3idS18uzpfQq28CzXW1KWgRYWoM9dzqkE3J/nGFcur6IW5WN1M8JxYhi5UeVOJyn
8dQhWFOO4tGpU0EY0cHqrUDgS2VRfuTOrDHPh3SeqVE8Mp8hiZFW8IILWCqtdBkG xTzldrngwCnNOn9agfUZLp+OTl6JxeltAaySl1ug2ygPyXSS03+mqL/yUdAqoASc
gylXKQz6Q8dD5aZ0eCHyHuEqR4wC+Qq/5cn8f3g5mSkPbQEngzLq6+DtszhUEqrr F0CZ5ZGJIhAxLnqtK276Ewpe5muYv8feZkpS0OSTCQ8S9I78W1DG4aXhldZjc6MK
IZNXfSFTgV3b0qKt/kBIDE/Mf+T3giUA19mMmgenvqSxpErK0kVnZLhuopQMqVHI E/j/CPNVaLQHrCjwWS8FO2utZzSGUhsuj2s0nvDikK54pNUnF+MWGXzQXnQHfByy
OWfZUbUHIXExpOhIW9Hu5ZhaRSq63KOv2JxAx42FkqSA9yLlGPsAoGG78ZiqEo9m LFCgqix1fEkj8McYUqI+ZgkaoiWSGadBuB04Vi8pr7XUWJWnp85vFX19kUH7xdmX
52f1lfkVlHxnpupHjK477NB/ZRo/xCVXLafk5r0LY9hC76cr5ZQOEPWptOFz705/ 4oALntdcnR4VKLgP9LsDMo+wle0RYyIt4YnPv3iZNrSd7yqvnuaucsA1ua3Wexsq
mUZuJZt7ZXWqHUe9EHJOYBKoIJOguT9/nOXFSf9BWkVV2ip0sMOy/d3bzQK7alcg EowdqoC8sZWZCrgb71yBgCyKg+QNZ+P4sMRllQt6WyuCh2x/jSbssPsqS4SNhCVN
ONSAS7fIOtXjDRvq2m3uovGq/GRor+vuKYW6CJ2vbZDikvF4fJMQvdIrNPbVKNX8 n1Nyh6qHkrb/2cXHHJQsPfO1o3NDnhmrVqNBVvtPB0q95ZHCvNj24y8eTYugD/l4
N/HAlRDVjCKOTbZ6wLjxrthrJ0pZJDvHasJ1dlPMI7sriGLynL+pX6KMNJQwDaVY 0zGE1IdAf0IT61WLK5FW8Vh8Bo9VHH/qA97BrZV7F1EVvfmqtaY0LFlaG53n26lv
5YcDlXevJQpzz5eDfLMMzvPoFQGmobOO0B+B4Eu/ETT5kj4fw2m8/YK9s4800K8y eVBkNFlFaliwqadQL2ZMQsdtwt0p7sVdvEjK0lwoRxoBWFU7ROpW2MrvjlAUx/JP
88TY+dcA2QKgfRbQ9FSzqLEM4i4mdf7QnQViyCE9v4AinPtNNb7n9gRAPsL2omJF hUQVuYcN+povfz/AZgFgS7CwwDb/cy8HxQu3pp8TL2FL90Hl1AuivX8fES9zE9pG
yX9ieGLmyNXMEdpiLyzKRSs5YIIaRjnzPaQYXWOFxsGoVsz6W3dVbUk8+l9FmKhW pWqZN5Q4gptYrGJFjah9uo8TA+10YhoJ/gpwbztFcatQal6YRGUXkHkIKfjkTkgO
2gFFSOceOmpxYvhSz2b97ysZpuD8aLTP8ULfA0+lFbIyzdeG5ALRvxlzhsOor5Ru upunOY8AP+OvYKce1FyrQJZqn3g/XjEwcRb7PY2DDwCIhQH4EhkGF19satcJhTTD
sa8kRmZQILMn1Tg6L+qkRiRM71NIPimS7yPmHKNoYCDN8IrBgfOgfQiTewxYG4wE OwMubBpSt6FAWWxB27+Ki5mtDc3N9BLFWc4cx9mqhvvFba3p2fwJJqhgxpb2YqhM
FxWGzOYSevTj0fg0FaW25rtwyl/UB9b519FPGg/r0y+Y04E9rXYrzvwblkCUegej 0Kl22bscyeleA1gxGlAWvKXfjzNbJ7EygXzzoMOPjDFSRgun4UBwPIoV9mQRsTV3
Dztu5tlMaf+iuqP8kZMcsKoNAYBUtAW4YqkSkQw0ZwE0BFCk12QUeqWFneU4DxYg hABOAcvK5NqGDgAiGYyDWlaZWxGScIYQTyPVWg6YE419+AOE+tLLdlDxm7b+cSbc
Ai4Y5XdNp0q6QYM3ckuuZFfdJX6q3tfZY9Ym0ydqprGjeSTyfEZwVxXFFk/lcZRS NYKVtPPPHiA/Q2mEuvugPEuKRh3OV3E7PiPr/IyPQHe0OpBmG/iuj24v0kyBk1yr
5mzmT4B6LVG3dVy8YYvpSfBddzxqrjCMByWh1EnNr3vU+Yl4opGCbcgHAXncHkUl qQk5TjjN/iYDrjsC+Wocb1YvE7zfP7KBHHs7XbUVFz3hvDV7nC/aqayqqd85aY5+
XxOu9kK0j6tJaDL7N3F1AiDuRGHoUPnvIw2794AmW5QfnXuqpoW+liLcjru1r8fq jx8C4vleHlEtFbY7amUTyymKVwp0ksam0JveEd5fMmgqsdTzvbNCKOJtFuFowXcH
O0B9SXJ6Wt2lJgtEUAC//g9/CCTqLK4QGCe6CKIJaXIO+9f2ylIhT8sZBK7YcUOP 0iPM/LCUyyM6Awu28aHLOvM0Q/z7My7f2wCOULnf1NampHRzslhLg5fNPnMmHptP
y5N88wUfThE8GkGkcQxuDEziPnq1hqD0PeYniv6i45VPU6M2K1vt5ab0CppqXUwm XQM4IJm/rkc+xyd0w5+2Y6y4k3Epq0kUJyLduWdv04DarowFeng8KvrcSJ98cSqU
0Qd7heb0fYUAa68CEnCTikULMkPlEv+gb7UoWaEXQyo7X+Gv21SJaz5k9xy1mi6P 2Xu4y3PXpeJ5ANC84QV0fYYXGb6gvYiU2VPRLyRtvgZbSSGfw8jbnv3IhZHKeOXd
zlma9ngTe5E18P5rhK4Q8mop3OzgMcjL24wUaRV9uZ/7/6i8WRefr6Su/frjTjGx mBdYacWmI8WmsWRfYUyXYX2bgEUPW/P515oYTSFzFp7hoN9RuehnKC/v3nskoPHO
BITWbB1aB1axNM5HbaTn5ZPIcaXE3a7T/X6yXjYBMlTZ8t/ZqHnQpiHqKWYUPuzj powFW0Jhs8TdSp+RhmR5Mww2BRtlbbHek/UmWas1SDXdHmtfYHmcQlPpraQmlEbm
zQ2+b0i+DkqtatF4z2NXkwLu1gM4BK7wr44V84z7bODRwU9L8l03f6pZyAl34NNM M0IyItVp0meA/AnoeD2AlTs4Ak6sV508u1WUKYdOEeHVuevRvQSMw74tI+9pe8HU
R4Hm4d0YfU3U9WYWQcNdQChaomyUqaX/caSXk35YdjVSg86JENaFuPon+QO3wCEu 3aMsk/KhUor10+xQ3QhABCV+VJP3Yr7Gvdj4D8ebipmDmdxvjqhIWp9UVMeLNYkk
7ZObwcsxDac/q+euVGFP2dWGwHFwCWGDwDNY0L9xHgkuZgLPNoGuJbfODLq8BcuG haQCAtOgmxBenfxmupae9iDmmLEQyXmlJ1Qp9KI3doXzEihPKwu4y4/0TuBeA4YZ
y+HpYBLjcmVYS7qB14JFsP5/4oFUCgt+FmfEuQw7FC4BNKsR4PH+R/iZPncm6Yl4 ZdG6JBbwGpRITLYuvBfZsDrnG9DcgOM4JeXukYZdaoauZQi36CnIqRUGOK4KxCpY
Lkc5Vw6tLj9EpARrjw1AvBl8JwZsMM4UenjK5L2y8TLV0SbCySsEaFJgvrmc66Me 8RaGZxoEDI+WmEvaIgTXzZKDeDy36i/jynp8PL3M2Mj1IY/+oBGgXAWDV5qzwMl2
RknqNhpe4rOtxRNJAHtLWxx/CB0rXOkuD/iO2Shtgc0dHJjhTG8Ii+TFjiPKRSml fG+5+4ASG7fYqr0P9QCChg9mtNqf6sClh2LRe44Ij+VHskAEt3gCzRS8wx/OnQ7P
xf5MF+wrIHEtfQvdh2VhN+uZBRqobXzeylX1Lck8YHM56fCIZ+/3xXD/EoiH5H6N ggApGFm1npKwLxSUjr+9FTpygldcHgkaF4aTuMveYeeeLzi5BCnPUHq+OvV0bDd/
sX6eETgh10ejjzb01XUBbGtW5+IKXiz9N9AHuiaHi0EzXXxm4IeZzMhPtKYXib3r /zRe468v+Tw7d+N3H1RZNX1pgEc0Ex0z+FagQety4xWaAWQyRUh9xSgP8GEquyC3
aoApdOIOb3YSlb3LQsqlx8Qhi+Rc9PJg6PiivWcZOAzRfLwtLD+zDTLhFsdbyXwP VFO1fyXfBRbrWHXSGAqNLAir8cNsU7fLN7eK1J6DAss/Tu7QXrzbvqH11Kkcvu2I
0jJC9mzg64HhvZx/15V0FJUz+uQhofP1u4YOXxJLWswplytBZycE1Kxexa7Fvpun 5Ju+MuiK7B6K905M7b2SH1qxpwgQB/e5OzV+LiYRqk2KJmKBvBCn67PtXom5kQm2
d4QyIu7fuzeF/Qyy2D8BEW3zR2mHSUKYmnTN7L4EowPqiIKpxSRkQMeglhze5HeT Q3JeAF5t8hnk6yABD7+tZ8X9MDDSkTCV2Un/ZUCqy3wfGoSomC7I4Yo6bWj01DE+
sDYyoo7c7UqJj5SKCgNvo7bnIfP9cEIfqpx8igj3/gEGoPZdZvGZ1eotYuatMgjB iUWoQwGb6QFDnocJod63Bwml7DpbsXqKMhVWygfMGFzQuTwRamUAGyjQaCnSOevL
fmUXITqzAyOYBdkRfhSnGO0J7OycqafKZrnPi+m6FLeE5+p+us7FJHr4Qwqfz2bY yMeopw5r+wJWRv8zeo9eqd/ORG8LbHzcU+o+Ao6z8dUcQUVtJP/B9FTpgXU+dQXW
eCu89fjjiKsRtAzo2u92AvffS12iUom5PpGA4h9KrGwXzIB3OwpJEWxBhFvNJ6KQ rir4Fyj9s7OwJ045TAaJk5zLUL5YtyUg3UcCJlA/OqPxgOedhCIzIB9QKLkgepfH
xignJ8rKxTYJmsNZJMfDTOws1hkwyKvfM3BfIw6TNugwSCSFW2j6s+dtYxCtmeau 4JeyWkljQylBG7WCxLPMKMfmx5gLNQ4HXsZZkTB7df30mmuCouBhhKW87I1hZ1J0
00walJ3XLRteQ1hNGtQU3/1BHa574tw34EkhvbVKfPht5OXQZ5vkWscuHDwJdqah EI6r7VsnGCHGTIxnkyNAnO3UILPKxsZUvQzKj5c1/mImTx9Eyjq7W+YoEsuIkQ5T
bvrFugA4Gu4lz/g5rJnK8r4t/0Mj89BxPqxJmEwyedzYhT230gqo638MoynpO8A8 mtVCIkI6PJ8mXoCyFcqnL5QdVdHZ5ZUHZ/MpMeG3xub86rhx9cbN0XKbR9FkoGWW
RM89VFgs6ecHpVmQDCQEQ61HyVsbWOs3y/Zj1rOS8CI0COctuPL+jjyojxMSSHnk lHcGjHeLRBK3xqhEoDL8qBEgcn/DnhJarDLrFWDi9Xtfjiw0wTv0Y0RqBvt8RYh3
1tPkXfqqUQg5cnc6u9FLkhLMPjnGRo/NylBQCr3S6gJ2Dh3KUiY6i29yNX2HEmp0 W4DgmmVbbFrkA9TEYyKOSpHulJsJXALWdGvj+1xJel9PPbkfh37qn/fljPi6bzdk
1HlXSIPgkuRJU+9mwKsUNIjio9w2w/tazxTsCO0SdKmMG429k8e+el+lsCplcLEU R0feHC/Y8yZBD1bmzAtUcYenogNCCYAvX3rxPCPQZBnL/GvztBNhddcoXPTOhItG
Mk1sHbMlQCspg/jXddrcZIpJBeeN3bXFG8FFsKRsFYqbok91NFYJlJu6JW+9cSxy ljhGHHRLsR9fpnE4n3WnIGkuTjmHtVDLxIdVhk1VWosket6VEe6bB2rfCKUa2unK
PlNCM/QSjyb1RVnMPl6LrvMlyqgCErkJ+Z04jWof02/V5rFPZ8MPYtIyqHumhLej 67RvwguED2+MZZPVgeQ1tYCgKm/OSNGdqtr3kXjNNtN2/YQ8P5wOCpQuu7e/xqRI
37RV+6ukWY8nzCKG1DrexEvW7moiUoif0eEDNWBv6aaI0MVBHR2gH4boUypGu65o qOg1HRldFAzi+QJhfOyQOr3t27MNTWMfEu237C7QJRYCYM0bLpeyjORVQUNbF7/Y
DBSoCyddygDGUZZprxBa0tYh1k+zQYeF7kTe+W/7+FE7ooQAiDcNRVMYOKhp597r vZZ72pIAGOQrSwtuKnVrfWeEOKUyKxoNYYzBOt8y/qTYHssi/xDl8Mmbb8dGbz5j
2PeYZj2vpS2p0SFdanCUE5aHfRB6dHFxV55K/ZavGn6i+9YDlLDOGjoVbn6Vpj5O o+ON31cnX7D9PgZnGGOgBKWyQb6JmjMT5pyaB4izfM3fv6z1hPlIFqfm9grS2lqA
+8t+QgWwDC+ioBH2dovyjzydTEAnyARDuwSARIg+q/4wczXWDP2338Y2osIYRvjq l7+bitgs7P45gAfLmE2NNIN2bp7hlz4RuSFfEJEsQvkH9hSO60BPg2M9UQWZdXiH
MM2cxKtQ/jrB15NfmkQgnrs+QJFkSwYvzSxcoRNbVp/PKNxsiy8Xl5bre7KUTDt1 jUATvdGa9vTbMSTt2+XGzmI9A2sxfVjEdGCHE7UXBd4mc4BgusbouW7uk+2dpPwt
zJQA0MWk02qgBUzttLTBtEWtwiDVNVepALq7+Hax9WH3HuMKLWZL9fXnZMis1CnJ 5CKtZY9t3g/SCMfF9/wBhs8Ov9OLc9N6anE+PiBgLRVj9/XVLvGL5n7g7By1GJbm
ZBB2uUUypBuFZQcUJnByt8DxcltlHAYFQhNI8/GGaCThoLG9PxZoJ5jOdy9Q2taY S/K24t+DKFfLfnjNi+/yovry41JSAQITYaVV1EKb4AJRqnVdRQ05JCIjogvZKNyS
zDf3LPCvTW0zkRkzLsI2JjkXLaYjSfd7nKwEgP8sSpRzubU9WwWQfHaDkGwEA9+J zTgapAkxRIRpgodesyZ8Ilm23IafqkmKQenIF6VNPJ6tDd98fljiBi21FQ3LFowQ
uG3zdlgOaLZ5EzxB1Igcw1TegsU3AwDEMhF66mdt0xC+epK9mm4RXkkvrC5DOWAv xj5ZXnkReY7vO0NS3uszpVdR59TUhoCJqkjL2CHAyXzubzeNQUaVe4C0KejiQ/fS
aDSgq+dvwPzR9i0kmo+6p/QMU9RZZx41zNMqsWK3TfBBagqREWRR+9HX1wX2ihjR WLgazUuUSA+/zvu4dRQkgasLU/OT/oIbaDuVrM3p1VFFp4QYnIj3XXaTjUGFSaVK
WzgM3HS0Aefy6teyKvbmUdXqJy+kI7lFaWJBKPWCnqNanmPvcuy6ck+eohvjdoBV LHS//of08uaX104y2kPuHHDrpD+lDy74fPPxARnTzXs4/vozLwGNn29BVAfKOklT
l2lVQyfszmm1Yk9XGrFk6gQGkfFE8yOk9YmmEvKaIU+K0CzcLqE4IDSsqMp4Ja/3 Psogtbuk4JZORBGBASgu6njoxT/JfUbtAEkf17YxvGWBLrgiFbAY8tiIQciA7ctM
I78WcAMUSBTr2PJhxfbT8yi5OnOfLAqqpxK2eHRVEmV/BKLdByyIAqIzEuM2rdhb Dd5rBYL6NfBKph1EyQcgDTKSH3N3IpxmWO2WusUX4QR8TOk9JJxjsgdzTXH6V08B
jchz22BVeak2k6+FV2LfKjTOEEFqHY7RY6PyS5PYJ/tnsGw1gVYJ/OdkAR5/byvT MsOE/P/uKNNLQ2e5mLEfBMADm+q2TIqdQp7rp801GSijh8mjp/dMMvCShhELxsnL
LTDintAJroi33G9cPT7EzrgT+lWQCFPlr4RF8fvh24p+QaE1KyaTwIugGbXFU9NM oyUWjby0RNl5OZLVbitxNZ/1Nz9X5+06ESPa68qXAt5wAuK0OaxvWjjRZraUUWv5
0QiKB8JmsnnuLEc0zdRJDzLVw53NtGO4z3pl6v1fA51PHbON7k7f9Ggr1fLlPGBp u+yOzmUQ7y+DtjK7/9GhIftcTpiY8c2zAQGDCwAeRknWc0dzetaa7+3qERhuJlvK
EhSfyXP58BMvwDuF8nViwmQlfN5aYWVbUVk5K2iUNuxSO35t3ibyuc+XqTiYS45f LNFn+kHJHwTJ879D9ypkSNxCiJMhz4nQkcvfaTo+w9dOWxBI3CYzfSyeNAM1Nd3h
TdqROEacxRO2dUgog5uiAsfOkSKSeoZgRh1L8UcjOUu3mdh3XQBXXQaBgdQeXjl4 5uD41U4oyL/LIcQ78CfGdofTsxoJH3hkk1wd769o+8PJW1Vg8+UoI/oz/boXtLNc
oRpyeXGGjTyHHXWXBCcjEBmAO6hEQOQb67hE8Iflg8hZLgzgb0IJjuOoeK4Whkej WPFmsq4lxoCrraXrbB78Dr7ag13Ny89X97KN/BVjSREWwnbbeT0i5UwgfVFgg6MR
BMCPy7rQPpFRyFgWioZEIunxqjF6Axv2Xr4G5QM+ITDrnJ7DXh41bGOJmifbWT+m pGbsNA2OHuVEdrgvOsLCrdRgPqYJb40aqYOn8IZPvdLMkaEx2WND9VLq9/kVQR81
007dyIfnu5Ukag2B+0G5xnYPZPmpLrStv5CWJ9m7SmyahyIUUuMu9gtZ50x8TOFL T7WYzwmZEx4kwpPxOkyFpEafuVokAOgsABwQZKKaj9HWcOiaSQjCKvIR+qxNyE5v
i2Pzam+b8xRZ5YdtzY0rDQ+2yWZvb+Ufl+wNcgh15+6hIZsYBHbZoa7eSkAbw+xM 5CEwy51Bg8j19wpzqljkDrJVFizgafs7whHMyvQZ2m9m4uDzvyZyydCS9RDaZyRk
xCA63Zwic6JrGtsQrNqBadmIgZVVsLwTFIgAG/0GQNp6w3H0mZ79tQnVbmEH3IWH lywiwB5jEqml9or+EnZqoc1qfgXGF9XrgrC2Zw6jzVuWgZUZQBt/mtgiqFweG6WX
41fWQEPr5ABSMPqfuorBD9zrpCoyWnQVqK9RQupigzC/RJEq8JNw44sftcFAM1Tb VwnWlZiiiN9dOtI+HkHiwkhmfqjam0m2sc2SbG8qhL48SiI+Ch65ZWZzPPu3o2S+
I7meSaSBIvCJmOCKAxNceqx8dRv7wxGohmjS/WGZeuuD5XX8C1gq6eVnquCQ40F9 7YG8nXlLG2jSpR0AZmeeLJKqmc9x2vbPcEA7bZeCesmY1kd9dN/fs1geasRe4adV
5YbcXqbwhJzHPJitD27+5EAaSlvNZW0mSjxMB2sw4kKZV8a4nveZXziG8RFXDE7Q TzgBtb/90a2i+ksjxGtQ+akZZ6B2Ag6yDjwIo68BgIJwkHlJYB1ZiCwHQwEL9W7y
b5VelR0GsFdktlvrv+HTxD//Zu5lBmA5z3KqBz7d8OqS8ZINIm7MHsnIz+3xDc6l TUJFofbO9ZWMcvdwrry6cRyrORjr9m6Mff74VzN11KOJHoU3Cp56vZ49WkeJGtxk
aMRIeSm7C7xb5zMJ/fzQxEZlmFOBL2WGctxP3Rtu1LQFRP7fDH2e5RIn/1R21uGn WuIVenjnvus158Nj82vxXcyYkW05ZhMZ5Gm8My336oDbGYHdC9TpyYS3HFp6vPSX
j+rjpD/htW/3EJOYgJtj2ikdOnZ9oPerfkkSOVDQPfnAFuNPML0Crz/QTxdw6bSS BdTTIO+4b5NfUxvqe2+C4GeGybIMH/js+x+9LitYMJpOjfPyN/RtYr2GrnuGslZp
hjLnrk9VHjAUMAX0Pk+G+l1LKi/AvZSIgTLsMevm6tR/DKT0Xbs8sNM76jSIyEd0 3M8FrpNZB2cWGB2v4lRJS0uozTGn0ZPBy1nmnsybksgfo/eRkNgE+LmxVPQDcio0
/bN303nCoobflfkzOeyCUCzgQ7PeDcKWftEta5TyhFeuldCOfAN0/1dSi13ChKcg eKGYZKDEQXTrGZ7l7RShf+Yz+5AH9ablHu2XqN1AhaXpxJ5l2LkLtUUpMfvs/uSs
YnzAnuJXgFkJoRXHMrtQoIf4rnek/ML1/Cx/UsUgSJB1Ut22CQwI+gyZDvTdOtO+ 5H4y/kU6uc9tIBwIcr5Bl55v8EpKBWn94aRoQnLdUPG2clyjDcF3tzVnLf3nB42b
dB+6Q2UcXwXIcnrUjIskumaKO3kHhAKWIxIKNBzxHfarUh8vFENZdZ12UhJtNH3l 5sp+h2XD15eS76csYM/N2OZaXp0ddjE7AVsYh1bFxVhC69jbdcPSZl9V4c3GVGPx
AzpDKI4/L6A5bcgWPLSXw2RSVI4ohUnXL7kg/0a7Yp9L3J+QkY7zeNgKdRIgMlVB rItQdMa/wpxYHdDvUNSReHuajZT7uQa2TPplIBcVJXJhKjQfkQSqpSYzwEMA3XFM
LQRPfgRjeJkY9/NUu6LCmc7ZcrXfGcJi5S1ugAqBiY1epOQbiLIfwLHqKOSraikP MbkqFGyyGBoe5N0cWVuc8HPdDfxvEaeaqhr8P0lBFtpW50oYIfq2bIvq7/CK6e3+
gHZRmmHeqr5xRQtFauMs10DrJQP0sqGC+2bGfh1RN98iNjTwYfuj+BWhqgmG3Lco bmNlach5UzZoRZ9JPtxGscKRi12nxGRtXHD87oI5nfGGse07/3j8xsaFDcsoZIgZ
OYtjEZN6Tv/PSxpKjbvZajBkBsYnxQJj1TAOOe61a4VuLMtAVRo2+OAinmef46xA Xp2/Vln+VJkaADk8y66Efji90agf/pWSCd7ujXbLVSdRF9y2mciZXa+MV4dggtCh
x5BdR2IpTL3u7pIBnAn5cdOCi/jMwZQAzP4LloetaRCu2iCzx7IVDVI1bKUgWdEF 1JKYq6TF8H8WKFOXqCyLLz4BKpdPn3BWuXxelIol9vZyNMvOHwR9FNXn4lWWZiX/
qVklcgRRFcGONRRFBTAsh5tlmwHtMMyvKyD5d4mmOHH4pyVzdhKCXCiFUph0+kAq ElwzTDELLvoGWz2UwiS2FhTFZuHSlG+th+IK73BwDEgw4/sQC491eujKVaMXxpY2
Gb1zGEZHM3V7mXFTRy9a4urYEYJfLL5prcsB4qmzaqfnKvJg2A== ngfMAsNG+v+hN6zHXjfo0d7r8qTOOVMIWyXdgsBBmKkKHA==

View File

@ -1,63 +1,44 @@
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 Rm3AOulV1eQ8pzOfXrkmDYIwLE49yNGH+w==
kWlBFqtEV39j0lBVf/9JOEayveFbVqzzqpwZ+azyo64XQt6CFH92WzOTiS7/HThS
S+46LDSfIs64VU0wEKDJpbbLP4vLjC3s/9qWFrQ7f97DjSZl+zv3pMldF8pInv8x
UaXNG3C7tQd1pD433MGXDWeygNZ2Fuv139lrjzwlXYkrq9OZg+bdeNm/uSfAQSsW
U39HMaZrgc8cANoFXIabPAtnePdS3ne+qr2JhYCGztYfyFQlaa97FeveYxqXMtt+
go8FgTvg7mBEtv6IAKwty/VFv2C7s5TCdExAfg2KQETjEDmSN8OtxPTx4Xqf7q8K
rVI1vsXDKsgwG4Uh8oEtWH6PaImbkD6Dx1751frsTIqJmP/WJithNiim/KA8+aBC
G0RTdl2oDEyyS64i6g7oscPkRjOFxfT0asdSLJcK1EFaZ3epyYNN2EXSLPeHZiRD
y8cv+dGq/7Oi0Vyw62jH9e+6uQyKHdP4oKJ58Dbm8B7KkY58tfCME7lK96uZtxiM
qPeeJmB/tYUu2O4HQhkmopixhTxnPiJkIx8qfVgKkrl/JqXYJH8R6Ud2bYVW4HYR
Tr2fkfQONaSx4DE85UI80A49KA5X+mZR8XOcJRhOPQRFmIM//goJlmGdcCQ3ddCp
r6/C8gTwbLXvcvJ6cvkyn0Z042sgj8i7m6eXQADgM/eQo4ki1SlmwxJgzC2350/5
dxjUitBLijrNvUeepX4xOSFkr2Wu3u7aQXt+19fokaP+U2wbXKqCNqrumDVBPWbP
mxnO7rX9wCb6+kfBJiQ4Tqbsh/TxMKVOzK4xFB0vgJOACkj6a5dVjOaJshVwrA28
/F35nwIBo3ig4LaT271gImo+XY36TZEe42MtSJ+Oopy1ENsK4Ii8gORi3lQhsozC
RdwN8jgL4ejc+NS6/a4bjLFrt1oMka2xPuIaNGpX/EzuSO+syk2sxk2vg52Qk5Oa
Zobc4MULf3/vT4rpgQuCUgyeHqzDFd3SlpXllpb6MXbP0K/ZNtg+VTJ6aGOZafKv
F49gm+bcC70SEMqIqHV2Fn0Z6DFhnbeFtq/uUUNN0bMelHghnvNNQ6FvPZOOmPdH
YcHX4LzrTtak7Xfu5lWT1h5leWG5iEHTpzj3Lvkdfb1Q38maqGGv7TQEQkHhQIfe
/RRwMmPObImKt129XslxTT7g5j0=

View File

@ -1,10 +0,0 @@
U2FsdGVkX19SrNDcNQsdjfo/tfZAOq9SOyciusnUhmQwo3TTr+19L5zWErgFPF7n
FLIPBXWOHWJp8S8NwGbwGmg22Pp7rvns/5QeMVlwrRZErsdcmW16OOx36s0fO6Yj
xH2wLL8P3YDsEdTRAKwWmjzng/DCFH+3xhy2NbpsdAoiDIQTXt33v0damhj7Mj0I
Yc2uoSzYjGuV/AMmIW56uR9GLY27dFT+AdbSoQThRHiy9EMjPDsyg1e4a1TjOje+
Ls/DO7nGpqIwZx2ZMbo8glTJPR+oYnzepmGNB3ehdLsivJirDxbsIc6Js9uE36er
qB4usUsIrz8H6HYNg/VHVQBNm4BhLDf9ij0NZC1dryuY/zBGgLYHcoVa9Bj7qhOn
A3tWtKc9/MgINf/kRlMdchKZG6sGRc3VcsB9mDYngJeuiShuysmCgajBCahdY4O1
Ctx9VSIFhVlkrlZoE6SoCw/z3MmJsJa/7Aoa2c2hzbIj+m6C0LyEHbCleP+6zCAF
YY5aJdXWbr2mDTOiaFYprtcYC4A7ZEgt3c51H+Lx/r/GLzhU28BYc81SL4+9LUwv
4cDJf/HNTxxp4YyDl52BZD5wS2tzlgwW5ekkXWC3OVpcC0Nlstrd6blHGVM=

View File

@ -1,3 +0,0 @@
U2FsdGVkX1+iMoNxF8ynmoanDXQhWQndNxROETBHF/DNS0Guiy+YKkV6GhpotHjY
0xWuD3VVFP8zwOx3rWfnlvd34+cqrjgbBoyXDh9Q+mlMMGd3HMHJ1UxnXZSJV5Jx
6+NIKbE=

View File

@ -1,5 +0,0 @@
U2FsdGVkX1/iadIyfkanSYsP/deFkKz7qDB6n1fgcrrwJRFipVuyd1R5ph4g9+fs
jI0a9x+VDI5BWiEVnG6jPHH0uYf5OvAEJp+lGAB2Qqs3TyNEQzAUTs80Ag2V6SuF
KPAiahzl0afwe37jWmjktO7nuMqc5aZZdF+SpgFW9rM6UCsOwe7DYXMOWxUZ+7Lu
f1aCBQOgSq1ISkY2if80RrsYqDiOED7GuEWSoxRA7oS3LWVE0Ieic3WdvHm1+4gP
r8hqrKB44MtDSg/GCSlECM0jBy0yMfEqlY45EaogS54LUphox/o3luVaV+A=

View File

@ -0,0 +1 @@
U2FsdGVkX18b5NlteArdllLCmrXQfit7yWS6pgZ4896+2BLosJFP0y/BauxAkNjU

View File

@ -0,0 +1,2 @@
U2FsdGVkX1+O0rTJQvBCLAdswjOexMM5VjlnGw3sVO/i1dYF/T0UrhTBhsqLboLA
jMv9wkzfbf2LiejBVhnOM0BEOU2FYRt5crZDkUAO090=

View File

@ -1,9 +1,9 @@
U2FsdGVkX190tK2fRtlU6XqGGKq0rzfIAZQz12ysC+Ltrjgwt95tduUxOcHr32x1 U2FsdGVkX1+Ay4K0S+xuJiyoMRj00oMkaw5sYVvu9VBR68aypK8eLTng8xoqKwzm
sXgMjc+ZjL76jQV5UVxCRSqpLj46lqJoAQX4CujTk1yoRbG0fZFGqgJr8OQIQGI3 uD1YNhLdh515CgHyMI7/LraT2yDYIlF+pNEfftH6U2qU5IfWSoukD59RscfaAft+
gkwVqtpMyxSE91o62IAmnHFztL+MUTJ+hZjpDo8IkcHDAZIXB7gRgrRUye1Jj0hV /dend9Y6HyG1WdlyPyLVabruHFScx3d+oaLwEcgggnI/M9coWnHyvBXspo6E75um
f6eDIaw5P0wzzq5y7fNzbNzM4cK5IjWQtOyhrpcsmjaTzy2S9qzKib3xtAqwMfUC gyvFntN4GmJLf1sMQIn0I7lW9djC8nupjSTstRo5HNLM/LwlhwAYRb/jbJUOkYSK
7or6RLdKWrDcUOZPJWp8nI/cbIITGpNA0hsSK+LV4gbSwzcfhyr00OCGHKMVTf/5 D/SDHW5p/9OrACQzAKHFB3mg4+9SufD+cju8qIAn9uFcyCJxkri6Mz+SGqdXtSgi
sEBFZmrcdsGxqvWe1D3Hf2CZS3e9iWzBlu4v78jHhLuXc/6+ltPFosZ5AwSJbnMo fEZE2r1aCUXFa8Nq+qoYbxVue3BFlzxetC7fZrx2zWnmkSgOn7LWDn6q3B3KWeT5
m7tTLPMWMmUYmD4E5I2znITrtg7dtPYXItqsTMoHQJFMX0Kst/U6TqFF3Jcxv55s om/g+Ph/RE4piKzm9m2jIx+0TlkUHlpOKAf4Xzwdaivmm6HaCNc5pt1Hw0le1fTW
uTS1p0I/kqldY4p8Fmz2XgNCgm0QvQu5UJukQkuAj0PVOdRtpOR47CqqF0basD82 JE+6BkXFDJz/8ytROujTGlMaMCB/JHgK04diEAnQJmNQnYVG03PxmRHmmqXc1czQ
LQ6EbLe863TL1hhdo+bP907A OnIzyUraBCpBsHSAVsN/afC8

View File

@ -1,3 +0,0 @@
U2FsdGVkX1/H11TSJKN/qICnL3UpcHDu7b7mnYfvFE76gKtmueKaKgT45+XTI+Ye
3IBFJDs2p7goQawjXjbN4IWS+6q+DQFnLXcOwQiJiMJtCipLLDImg/TrK/9yyiC1
M7AmptY=

View File

@ -1,3 +1,3 @@
U2FsdGVkX1/UgigdAI8G1DADfHlOdqKX2TLIy/33gEUA+sADMY2MjCsBnVgirHe6 U2FsdGVkX19/6c3AzyWTN5p17ujhKlbDdk91iQs9z7Q0HiyA4L7BKFT/ZOk4VNqY
oFyts9qlwfUmRjiIiB1GYm0GhZ9YkgzZLgRSw7sSqtqiyjt6glZd7OXwt3/pyTs8 2Yh7r0b1F1ScFjvKH3aJ7jYGHT0i+w3LSHsufCDATEUejN/Z9JtEIXYaodOCJaYE
TY7U3rA= YuIaLuBwWdkAPUl1lhs=

View File

@ -1,3 +1,3 @@
U2FsdGVkX1+j3nwfqf4SUkH2HoSAMqkoYJYgRDPRknNOur7PZ5Ctx+eh2RYufCXd U2FsdGVkX1/StXpT1GfebxPB+1TyCHLo5fjFZLNkkWXnCS04WnREE2xlV7OXw0Iq
5yrTs/23jetpqjBGztmrPwZcN1AioGDEMKruyI+Cpr1RLE8SKrRjSvTXXK7nJHhI llqZTflZ/z1hSz7NuUO/vrR57RRo6icf3UXnxvJ8HD6Z9q7uxI+WpIj+ME2zij6B
Fw== Jg==

View File

@ -1,2 +1,7 @@
U2FsdGVkX1/284cSqdL3Tn4Yv682x+kNJ4OMUrOgaD05vJpoQ284b8Bji4PYvOYG U2FsdGVkX19J8VE7lArWiwLIURQ8NjPEUwkOAh4m1oR0yrmBCI5u/vhwSQTC+ETb
UazEOc260aNPofw= S3b80dqDKkR8QKRxIaquJHw/KRvQqKViZbu1OsHQTPQhK//mvs6vZ8G00vfucphc
6XIuiJS0u1zbzP6CKoLlkUyVOxFsmVSmxRx8460vgqK00JHSXf82mCAXcePVfHX9
uV9w34x3QkqSzmptx1orJrWa/Y/+et19ghJ/d6Utll+kg5Ldkd6vYcSA5bYFMe6L
LzAjJDLSvRpGkwP7EH2/9Kin5qDA7OUQmrXyFvmb9viCnYSD4TUaxXHYy4SPYcPY
qgFFeNDry5PAhkqLCTKgQWylCZXNbnA7JHp5fdbQCyRFD2sNxVN9ptuqNJd5x+hf
0PzTFokhgtE=

View File

@ -1,3 +1,3 @@
U2FsdGVkX1//NF0E4XObFG2/rvsrFG7AG6OaLwJDSaJlOed/DbGkXE1d7bTa9SX1 U2FsdGVkX18yVvW7ZvcS0Xc/LsBJmDBTjmHeODQqsVSq8AlzjHH0Z15cY2ibL0+2
7g7SeTKm7KdXsb8vxyjWbjRIBw/RQln3IM77bdDBCSCmsLp+HsHndv0QeXSUhSMP /fq+Sb12nfYhXkdFePGNJl+pwTVN2KmQhQtTPUawwa0bmvqC3wPXmHn8O1AndVP9
rg== 8g==

View File

@ -1,3 +1,3 @@
U2FsdGVkX18Qd6gBIE7im5jGZlFK2r9QRPtRj/MtweDdMtXPxO/JbN8zxlGIibcx U2FsdGVkX19MH3jJZJHEhLZLqIGcQCvd7JS2I8vWztP1Htde6A/xfy3zP8U6NUOc
5XxR5dtAQE5++pBw1mY8nxrtZIJLAWSS4r3TWDQnNr7XSM3wP8/kxCUL7KvhJ0d+ QPBYfycwXLqUM89gVrKnnj28HQiAzQNf2zzPqG7MOpQKA6zdRF6i9n+CGtvXC36u
Tw== zQ==

View File

@ -1,3 +1,3 @@
U2FsdGVkX1/OVD62ZQPn1AUSo2ZWvjHbmfv9x52rv//gH+5wO0bApyxtHHsCZe63 U2FsdGVkX18X2ltRnCWQnXMSt/FSKiq/ScbhdjFP4wmPHi5njgtam/c1Dg+0T1fj
DbfdwiQNPCYKZOYvvf6tzqQsxbZwN0kBeyWKmxK35ZZpuVqA1RGgMB1pk7Ue1iBQ JzOYe53LglUBfjDMbIepcIymHXPteizligpJzNE7DwuzsCp2JTkn9KWzKJb45Qa/
z9Ta00qOXeWw /UtVdTfkS9WH

View File

@ -1,2 +0,0 @@
U2FsdGVkX186wcN9NuydqFDXes3OG2eoric99wmndrgVNV0RXxmEYK3MRkJHFYwj
24nAVlJ8yc8jzXbd7tcewDCzXn0Ac1ERKsuxvFw=

View File

@ -1,6 +0,0 @@
U2FsdGVkX19+JQDp/hdBBgL5TR0tiYujBpbUQ3e7ArhQI9xbGKeRiKi4Bk4tw8rV
MrxwU3Fk95sY75vsnU3uvkMSo6KFVbiOLiGTiFwnT3gwwHWKem1yxJCLmxcP+h4G
SGu8lcGpM4ZUy2yAnt7WowyzQiYmO0Vp8xP1RCmH0z2UdcDhqZB9LjKgEnpVSC4I
i5Se0fX9PB5/oWMCc0kPX9XYz0+/hPlgzcbaS6GT8mN0o08rHtMhN2gvV/xlONZ4
V0JXg7SYuTXz8cRtjLIr3sIwCOU+uBqrIHHvtjFclto0/zsFtfa00FomIMFDCv40
UHE2e7HJc4EXQT55QlcIbL4PdtxTI5gp+Id+eSI6vZF+dPWHKYO4Ug==

View File

@ -1,2 +0,0 @@
U2FsdGVkX1/r1JwvxVZfT2snMpDmQdldp0FvWJ+szrSoIpvmW0MRzqu9t5sC/hvC
XZPhTw7lwakhlw+sERpjQBxBN5TFVy1OBenOERtnnvW9D+E=

10
secrets/misc/asjon.env Normal file
View File

@ -0,0 +1,10 @@
U2FsdGVkX18ufnreJQQJJ52gMxajdK5bLn8A7Gqb3OvqThlWWb5mo4UV+VqEf/ob
VSydFr03zlSYuAuyvpHcunlTHnJR6RPgEdv0qV2NFBaVAlVjqJDgZHPKNLCp2Zws
LOgrWiaRGTKrBAD/80JlzsFyk5YVSXd9fTqo05bTym8qKv39vFrrmZQu1SOKRKmn
qrIUr9MjG25iLCxR6ajcANgfb3+hgQMo5ypr7AwjMp1PwkU/IWf1atIWFJzf0ZU1
4JOsDB4FvuX0hdi8J8LKRe+t0hsjQxb4FS3sMWrSDKhjvjRP+AEEwdj/3YbX856i
l9h2Yd36BtKOrwgrMQTS0pHvnUwj+o/4KeFrteccwgJP5bBJYVts10vg52FldNTg
qTrnnjVrjVm/by8Of435ttSXNmqn5g10MUKKLIIgNZXJcUY/fW4v07xduDHFMUYA
YJWfOfyR4Jlb2lJjmG0VwgPqhVMLAqFrL8XLGlqv1D/nKchktwp58cOqo95js+BT
Q8yvEzMbbtPM4MIGUhzfMbVhXFMmQRgfSpQFAPHe/33V0Ddsp7nCj0n7P+g0b8Ka
2BBS8ez8+7DCyTIerKCwB2+Hu9vy1bkhr8ugZXbmxvL3+fSHgMJ//KWFClQ=

2
secrets/misc/ydns.env Normal file
View File

@ -0,0 +1,2 @@
U2FsdGVkX1+nBNkZvBovUtzVk+hzQxFfQJ2NoORch7iPe33Zf+UIKOqkAWK3hjgb
aYDcTVL3ef1iD4saMpueUpoz36+TtwXAowPzGq0+BVLDyVikU9LM6QBlQQ==

View File

@ -1,2 +0,0 @@
U2FsdGVkX19LFU3NQZnhBBFTrDOdHX2yWfKjrFlvjtajfTZQhkpXIxRDiUnUntr4
lZ5dHXrJok1O7bWlVKCZ59zLtA+e5hHBrjxSnYIErWvnUUDO

View File

@ -1,2 +1,2 @@
U2FsdGVkX1/pO8dHm5elQ+5CIICbNLeWCuO5rVEPX6Are8Hxj9SYXfM47dWUTm4z U2FsdGVkX19YVs+neL4R4JDT1CSsndTtbggYoDxEF2iwRCRDJRtrBBJthnxRrUsr
wLMdCqxgQBKTxxfs7l+P9RCDLG03K8d6Z9Aqw5S9j1oFoxxANQ== c+A5NSSRRAu0LQ5vjaHlOYiCtmVCdYu7ECrpHQ40KqYgYhXJAw==

View File

@ -1,3 +0,0 @@
U2FsdGVkX19/kwiv3qT/dPLvt7EyuI9Bq8A8G3EPIHDqAOZNBnVpjoYL5Lohhjn1
rAlHir5QMigMm6nNBF0dIYh4vIKyWtD4g+6btiagcke3MYCtcz3zVdeZdzpVOHvP
tRoTA9ZQNw==

View File

@ -1,2 +0,0 @@
U2FsdGVkX1+N08asPnuy9GeOFSnq7pgilg4VwkPidE3n6qu934b9crw009t05+kE
kTfFouHdrcIDLX+Gry1kn3WSZNgIT47A8N9tSE2h33aRRqJV

View File

@ -1,3 +1,3 @@
U2FsdGVkX18ukCHRMwgdh9+FALCgM9f4u6hqx2xC/6OB/XhMSkRHllfF5GUJ6kYk U2FsdGVkX1+1zBjw7Y2NlBeTLcGS8o3Er/ngQMU57HLCN8jSfKBU0/C4o9D4NDjl
O6UyamMNnMyrg0Us2RkP4ax95HskaPSt6uy7DsmV53cZ0hpoxQAfN+SxBLOU36TW C7pRu3oOHmz0Pn9ipLaP87ST9RzVncHw/kqNBh8Dg29n3jNoTdSfwTn6xV/mBwQO
RhMdt6gT/t/zN+yBwFStJi13oCVQXWDMQA== a4OsKusYMI/dCriATixomxe1GkC06YfwJg==

View File

@ -1,6 +1,6 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i bash --pure #! nix-shell -i bash --pure
#! nix-shell -p bash openssl git unixtools.column perl #! nix-shell -p bash openssl git unixtools.column
set -euo pipefail set -euo pipefail
# #
@ -18,15 +18,37 @@ set -euo pipefail
##### CONSTANTS ##### CONSTANTS
# the release version of this script # the release version of this script
readonly VERSION='2.2.0' readonly VERSION='2.0.0'
# the default cipher to utilize # the default cipher to utilize
readonly DEFAULT_CIPHER='aes-256-ctr' readonly DEFAULT_CIPHER='aes-256-ctr'
# arguments of the openssl enc command # the openssl options to encrypt/decrypt the files
readonly ENCRYPT_OPTIONS='-pbkdf2 -iter 200000 -pass env:ENC_PASS' # shellcheck disable=SC2016
readonly ENCRYPT_OPTIONS='-$cipher -pbkdf2 -iter 200000'
##### FUNCTIONS # regular expression used to test user input
readonly YES_REGEX='^[Yy]$'
## Repository Metadata
# whether or not transcrypt is already configured
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
# the current git repository's top-level directory
readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null)
# whether or not a HEAD revision exists
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
# https://github.com/RichiH/vcsh
# whether or not the git repository is running under vcsh
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
# whether or not the git repository is bare
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null)
## Git Directory Handling
# print a canonicalized absolute pathname # print a canonicalized absolute pathname
realpath() { realpath() {
@ -56,46 +78,21 @@ realpath() {
fi fi
} }
# establish repository metadata and directory handling # the current git repository's .git directory
# shellcheck disable=SC2155 RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
gather_repo_metadata() { readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
# whether or not transcrypt is already configured
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
# the current git repository's top-level directory # the current git repository's gitattributes file
readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) readonly CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile)
if [[ $CORE_ATTRIBUTES ]]; then
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
else
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi
# whether or not a HEAD revision exists ##### FUNCTIONS
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
# https://github.com/RichiH/vcsh
# whether or not the git repository is running under vcsh
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
# whether or not the git repository is bare
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null || printf 'false')
# the current git repository's .git directory
readonly RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
# Respect transcrypt.crypt-dir if present. Default to crypt/ in Git dir
readonly CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
# respect core.hooksPath setting, without trailing slash. Fall back to default hooks dir
readonly GIT_HOOKS=$(git config core.hooksPath | sed 's:/*$::' 2>/dev/null || printf "%s/hooks" "${RELATIVE_GIT_DIR}")
# the current git repository's gitattributes file
local CORE_ATTRIBUTES
CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile 2>/dev/null || git config --get --path core.attributesFile 2>/dev/null || printf '')
if [[ $CORE_ATTRIBUTES ]]; then
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
else
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi
}
# print a message to stderr # print a message to stderr
warn() { warn() {
@ -117,209 +114,26 @@ die() {
exit "$st" exit "$st"
} }
# The `decryption -> encryption` process on an unchanged file must be
# deterministic for everything to work transparently. To do that, the same
# salt must be used each time we encrypt the same file. An HMAC has been
# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
# (keyed with a combination of the filename and transcrypt password), and
# then use the last 16 bytes of that HMAC for the file's unique salt.
git_clean() {
filename=$1
# ignore empty files
if [[ ! -s $filename ]]; then
return
fi
# cache STDIN to test if it's already encrypted
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
tee "$tempfile" &>/dev/null
# the first bytes of an encrypted file are always "Salted" in Base64
# The `head + LC_ALL=C tr` command handles binary data in old and new Bash (#116)
firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0')
if [[ $firstbytes == "U2FsdGVk" ]]; then
cat "$tempfile"
else
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
openssl_path=$(git config --get --local transcrypt.openssl-path)
salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16)
openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1)
if [ "$openssl_major_version" -ge "3" ]; then
# Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133
(
echo -n "Salted__" && echo -n "$salt" | perl -pe 's/(..)/chr(hex($1))/ge' &&
# Encrypt file to binary ciphertext
ENC_PASS="$password" "$openssl_path" enc -e -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
) |
openssl base64
else
# Encrypt file to base64 ciphertext
ENC_PASS="$password" "$openssl_path" enc -e -a -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
fi
fi
}
git_smudge() {
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
openssl_path=$(git config --get --local transcrypt.openssl-path)
tee "$tempfile" | ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a 2>/dev/null || cat "$tempfile"
}
git_textconv() {
filename=$1
# ignore empty files
if [[ ! -s $filename ]]; then
return
fi
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
openssl_path=$(git config --get --local transcrypt.openssl-path)
ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a -in "$filename" 2>/dev/null || cat "$filename"
}
# shellcheck disable=SC2005,SC2002,SC2181
git_merge() {
# Get path to transcrypt in this script's directory
TRANSCRYPT_PATH="$(dirname "$0")/transcrypt"
# Look up name of local branch/ref to which changes are being merged
OURS_LABEL=$(git rev-parse --abbrev-ref HEAD)
# Look up name of the incoming "theirs" branch/ref being merged in.
# TODO There must be a better way of doing this than relying on this reflog
# action environment variable, but I don't know what it is
if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then
THEIRS_LABEL=$(echo "$GIT_REFLOG_ACTION" | awk '{print $2}')
fi
if [[ ! "$THEIRS_LABEL" ]]; then
THEIRS_LABEL="theirs"
fi
# Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged
echo "$(cat "$1" | "${TRANSCRYPT_PATH}" smudge)" >"$1"
echo "$(cat "$2" | "${TRANSCRYPT_PATH}" smudge)" >"$2"
echo "$(cat "$3" | "${TRANSCRYPT_PATH}" smudge)" >"$3"
# Merge the decrypted files to the temp file named by $2
git merge-file --marker-size="$4" -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3"
# If the merge was not successful (has conflicts) exit with an error code to
# leave the partially-merged file in place for a manual merge.
if [[ "$?" != "0" ]]; then
exit 1
fi
# If the merge was successful (no conflicts) re-encrypt the merged temp file $2
# which git will then update in the index in a following "Auto-merging" step.
# We must explicitly encrypt/clean the file, rather than leave Git to do it,
# because we can otherwise trigger safety check failure errors like:
# error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting.
# To re-encrypt we must first copy the merged file to $5 (the name of the
# working-copy file) so the crypt `clean` script can generate the correct hash
# salt based on the file's real name, instead of the $2 temp file name.
cp "$2" "$5"
# Now we use the `clean` script to encrypt the merged file contents back to the
# temp file $2 where Git expects to find the merge result content.
cat "$5" | "${TRANSCRYPT_PATH}" clean "$5" >"$2"
}
# shellcheck disable=SC2155
git_pre_commit() {
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
tmp=$(mktemp)
IFS=$'\n'
slow_mode_if_failed() {
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
# Skip symlinks, they contain the linked target file path not plaintext
if [[ -L $secret_file ]]; then
continue
fi
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
local firstbytes=$(git show :"${secret_file}" | head -c8)
# An empty file does not need to be, and is not, encrypted
if [[ $firstbytes == "" ]]; then
: # Do nothing
# The first bytes of an encrypted file must be "Salted" in Base64
elif [[ $firstbytes != "U2FsdGVk" ]]; then
printf 'Transcrypt managed file is not encrypted in the Git index: %s\n' "$secret_file" >&2
printf '\n' >&2
printf 'You probably staged this file using a tool that does not apply' >&2
printf ' .gitattribute filters as required by Transcrypt.\n' >&2
printf '\n' >&2
printf 'Fix this by re-staging the file with a compatible tool or with'
printf ' Git on the command line:\n' >&2
printf '\n' >&2
printf ' git rm --cached -- %s\n' "$secret_file" >&2
printf ' git add %s\n' "$secret_file" >&2
printf '\n' >&2
exit 1
fi
done
}
# validate file to see if it failed or not, We don't care about the filename currently for speed, we only care about pass/fail, slow_mode_if_failed() is for what failed.
validate_file() {
secret_file=${1}
# Skip symlinks, they contain the linked target file path not plaintext
if [[ -L $secret_file ]]; then
return
fi
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
# The first bytes of an encrypted file are always "Salted" in Base64
local firstbytes=$(git show :"${secret_file}" | head -c8)
if [[ $firstbytes != "U2FsdGVk" ]]; then
echo "true" >>"${tmp}"
fi
}
# if bash version is 4.4 or greater than fork to number of threads otherwise run normally
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]] && [[ "${BASH_VERSINFO[1]}" -ge 4 ]]; then
num_procs=$(nproc)
num_jobs="\j"
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
while ((${num_jobs@P} >= num_procs)); do
wait -n
done
validate_file "${secret_file}" &
done
wait
if [[ -s ${tmp} ]]; then
slow_mode_if_failed
rm -f "${tmp}"
exit 1
fi
else
slow_mode_if_failed
fi
rm -f "${tmp}"
unset IFS
}
# verify that all requirements have been met # verify that all requirements have been met
run_safety_checks() { run_safety_checks() {
# validate that we're in a git repository # validate that we're in a git repository
[[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?' [[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?'
# exit if transcrypt is not in the required state # exit if transcrypt is not in the required state
if [[ $ignore_config_status ]]; then if [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
: # no-op, no need to check $CONFIGURED status
elif [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
die 1 'the current repository is not configured' die 1 'the current repository is not configured'
elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then
die 1 'the current repository is already configured; see --display' die 1 'the current repository is already configured; see --display'
fi fi
# check for dependencies # check for dependencies
for cmd in {column,grep,mktemp,"${openssl_path}",sed,tee}; do for cmd in {column,grep,mktemp,openssl,sed,tee}; do
command -v "$cmd" >/dev/null || die 'required command "%s" was not found' "$cmd" command -v $cmd >/dev/null || die 'required command "%s" was not found' "$cmd"
done done
# ensure the repository is clean (if it has a HEAD revision) so we can force # ensure the repository is clean (if it has a HEAD revision) so we can force
# checkout files without the destruction of uncommitted changes # checkout files without the destruction of uncommitted changes
if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
# ensure index is up-to-date before dirty check
git update-index -q --really-refresh
# check if the repo is dirty # check if the repo is dirty
if ! git diff-index --quiet HEAD --; then if ! git diff-index --quiet HEAD --; then
die 1 'the repo is dirty; commit or stash your changes before running transcrypt' die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
@ -329,20 +143,24 @@ run_safety_checks() {
# unset the cipher variable if it is not supported by openssl # unset the cipher variable if it is not supported by openssl
validate_cipher() { validate_cipher() {
local list_cipher_commands local list_cipher_commands
list_cipher_commands="${openssl_path} enc -ciphers" list_cipher_commands='openssl enc -ciphers'
remove_dash() {
sed 's#\(^\| \)-#\1#g'
}
local supported local supported
supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx -- "-$cipher") || true supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true
if [[ ! $supported ]]; then if [[ ! $supported ]]; then
if [[ $interactive ]]; then if [[ $interactive ]]; then
printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher" printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
$list_cipher_commands | column -c 80 $list_cipher_commands | remove_dash | column -c 80
printf '\n' printf '\n'
cipher='' cipher=''
else else
# shellcheck disable=SC2016 # shellcheck disable=SC2016
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands" die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$($list_cipher_commands | remove_dash)"
fi fi
fi fi
} }
@ -382,7 +200,7 @@ get_password() {
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
local password_length=30 local password_length=30
local random_base64 local random_base64
random_base64=$(${openssl_path} rand -base64 $password_length) random_base64=$(openssl rand -base64 $password_length)
password=$random_base64 password=$random_base64
else else
printf 'Password: ' printf 'Password: '
@ -458,73 +276,100 @@ stage_rekeyed_files() {
# save helper scripts under the repository's git directory # save helper scripts under the repository's git directory
save_helper_scripts() { save_helper_scripts() {
mkdir -p "${CRYPT_DIR}" mkdir -p "${GIT_DIR}/crypt"
local current_transcrypt openssl_command="openssl enc $ENCRYPT_OPTIONS -pass env:ENC_PASS"
current_transcrypt=$(realpath "$0" 2>/dev/null)
echo '#!/usr/bin/env bash' > "${CRYPT_DIR}/transcrypt"
tail -n +4 "$current_transcrypt" >> "${CRYPT_DIR}/transcrypt"
# make scripts executable # The `decryption -> encryption` process on an unchanged file must be
for script in {transcrypt,}; do # deterministic for everything to work transparently. To do that, the same
chmod 0755 "${CRYPT_DIR}/${script}" # salt must be used each time we encrypt the same file. An HMAC has been
done # proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
} # (keyed with a combination of the filename and transcrypt password), and
# then use the last 16 bytes of that HMAC for the file's unique salt.
# save helper hooks under the repository's git directory cat <<-'EOF' >"${GIT_DIR}/crypt/clean"
save_helper_hooks() {
# Install pre-commit-crypt hook script
[[ ! -d "${GIT_HOOKS}" ]] && mkdir -p "${GIT_HOOKS}"
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
cat <<-'EOF' >"$pre_commit_hook_installed"
#!/usr/bin/env bash #!/usr/bin/env bash
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64 filename=$1
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '') # ignore empty files
CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}") if [[ -s $filename ]]; then
"${CRYPT_DIR}/transcrypt" pre_commit # cache STDIN to test if it's already encrypted
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
tee "$tempfile" &>/dev/null
# the first bytes of an encrypted file are always "Salted" in Base64
read -n 8 firstbytes <"$tempfile"
if [[ $firstbytes == "U2FsdGVk" ]]; then
cat "$tempfile"
else
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16)
ENC_PASS=$password @openssl_command@ -e -a -S "$salt" -in "$tempfile"
fi
fi
EOF EOF
# Activate hook by copying it to the pre-commit script name, if safe to do so cat <<-'EOF' >"${GIT_DIR}/crypt/smudge"
pre_commit_hook="${GIT_HOOKS}/pre-commit" #!/usr/bin/env bash
if [[ -f "$pre_commit_hook" ]]; then tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
printf 'WARNING:\n' >&2 trap 'rm -f "$tempfile"' EXIT
printf 'Cannot install Git pre-commit hook script because file already exists: %s\n' "$pre_commit_hook" >&2 cipher=$(git config --get --local transcrypt.cipher)
printf 'Please manually install the pre-commit script saved as: %s\n' "$pre_commit_hook_installed" >&2 password=$(git config --get --local transcrypt.password)
printf '\n' tee "$tempfile" | ENC_PASS=$password @openssl_command@ -d -a 2>/dev/null || cat "$tempfile"
else EOF
cp "$pre_commit_hook_installed" "$pre_commit_hook"
chmod 0755 "$pre_commit_hook" cat <<-'EOF' >"${GIT_DIR}/crypt/textconv"
fi #!/usr/bin/env bash
filename=$1
# ignore empty files
if [[ -s $filename ]]; then
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
ENC_PASS=$password @openssl_command@ -d -a -in "$filename" 2>/dev/null || cat "$filename"
fi
EOF
# make scripts executable
for script in {clean,smudge,textconv}; do
chmod 0755 "${GIT_DIR}/crypt/${script}"
sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
done
} }
# write the configuration to the repository's git config # write the configuration to the repository's git config
save_configuration() { save_configuration() {
save_helper_scripts save_helper_scripts
save_helper_hooks
# write the encryption info # write the encryption info
git config transcrypt.version "$VERSION" git config transcrypt.version "$VERSION"
git config transcrypt.cipher "$cipher" git config transcrypt.cipher "$cipher"
git config transcrypt.password "$password" git config transcrypt.password "$password"
git config transcrypt.openssl-path "$openssl_path"
# write the filter settings. Sorry for the horrific quote escaping below... # write the filter settings
# shellcheck disable=SC2016 if [[ -d $(git rev-parse --git-common-dir) ]]; then
git config filter.crypt.clean '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean %f' # this allows us to support multiple working trees via git-worktree
# shellcheck disable=SC2016 # ...but the --git-common-dir flag was only added in November 2014
git config filter.crypt.smudge '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt smudge' # shellcheck disable=SC2016
# shellcheck disable=SC2016 git config filter.crypt.clean '"$(git rev-parse --git-common-dir)"/crypt/clean %f'
git config diff.crypt.textconv '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt textconv' # shellcheck disable=SC2016
# shellcheck disable=SC2016 git config filter.crypt.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge'
git config merge.crypt.driver '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge %O %A %B %L %P' # shellcheck disable=SC2016
git config diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
else
# shellcheck disable=SC2016
git config filter.crypt.clean '"$(git rev-parse --git-dir)"/crypt/clean %f'
# shellcheck disable=SC2016
git config filter.crypt.smudge '"$(git rev-parse --git-dir)"/crypt/smudge'
# shellcheck disable=SC2016
git config diff.crypt.textconv '"$(git rev-parse --git-dir)"/crypt/textconv'
fi
git config filter.crypt.required 'true' git config filter.crypt.required 'true'
git config diff.crypt.cachetextconv 'true' git config diff.crypt.cachetextconv 'true'
git config diff.crypt.binary 'true' git config diff.crypt.binary 'true'
git config merge.renormalize 'true' git config merge.renormalize 'true'
git config merge.crypt.name 'Merge transcrypt secret files'
# add a git alias for listing encrypted files # add a git alias for listing encrypted files
git config alias.ls-crypt "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'" git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
} }
# display the current configuration settings # display the current configuration settings
@ -551,7 +396,6 @@ clean_gitconfig() {
git config --remove-section transcrypt 2>/dev/null || true git config --remove-section transcrypt 2>/dev/null || true
git config --remove-section filter.crypt 2>/dev/null || true git config --remove-section filter.crypt 2>/dev/null || true
git config --remove-section diff.crypt 2>/dev/null || true git config --remove-section diff.crypt 2>/dev/null || true
git config --remove-section merge.crypt 2>/dev/null || true
git config --unset merge.renormalize git config --unset merge.renormalize
# remove the merge section if it's now empty # remove the merge section if it's now empty
@ -562,20 +406,6 @@ clean_gitconfig() {
fi fi
} }
# Remove from the local Git DB any objects containing the cached plaintext of
# secret files, created due to the setting diff.crypt.cachetextconv='true'
remove_cached_plaintext() {
# Delete ref to cached plaintext objects, to leave these objects
# unreferenced and available for removal
git update-ref -d refs/notes/textconv/crypt
# Remove ANY unreferenced objects in Git's object DB (packed or unpacked),
# to ensure that cached plaintext objects are also removed.
# The vital sub-commands equivalents we require this `gc` command to do are:
# `git prune`, `git repack -ad`
git gc --prune=now --quiet
}
# force the checkout of any files with the crypt filter applied to them; # force the checkout of any files with the crypt filter applied to them;
# this will decrypt existing encrypted files if you've just cloned a repository, # this will decrypt existing encrypted files if you've just cloned a repository,
# or it will encrypt locally decrypted files if you've just flushed the credentials # or it will encrypt locally decrypted files if you've just flushed the credentials
@ -589,7 +419,7 @@ force_checkout() {
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO" cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
IFS=$'\n' IFS=$'\n'
for file in $encrypted_files; do for file in $encrypted_files; do
rm -f "$file" rm "$file"
git checkout --force HEAD -- "$file" >/dev/null git checkout --force HEAD -- "$file" >/dev/null
done done
unset IFS unset IFS
@ -603,8 +433,7 @@ flush_credentials() {
if [[ $interactive ]]; then if [[ $interactive ]]; then
printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n' printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n'
printf 'All previously decrypted files will revert to their encrypted form, and your\n' printf 'All previously decrypted files will revert to their encrypted form.\n\n'
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
printf 'Proceed with credential flush? [y/N] ' printf 'Proceed with credential flush? [y/N] '
read -r answer read -r answer
printf '\n' printf '\n'
@ -617,8 +446,6 @@ flush_credentials() {
if [[ $answer =~ $YES_REGEX ]]; then if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig clean_gitconfig
remove_cached_plaintext
# re-encrypt any files that had been previously decrypted # re-encrypt any files that had been previously decrypted
force_checkout force_checkout
@ -634,8 +461,7 @@ uninstall_transcrypt() {
if [[ $interactive ]]; then if [[ $interactive ]]; then
printf 'You are about to remove all transcrypt configuration from your repository.\n' printf 'You are about to remove all transcrypt configuration from your repository.\n'
printf 'All previously encrypted files will remain decrypted in this working copy, but your\n' printf 'All previously encrypted files will remain decrypted in this working copy.\n\n'
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
printf 'Proceed with uninstall? [y/N] ' printf 'Proceed with uninstall? [y/N] '
read -r answer read -r answer
printf '\n' printf '\n'
@ -648,30 +474,11 @@ uninstall_transcrypt() {
if [[ $answer =~ $YES_REGEX ]]; then if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig clean_gitconfig
if [[ ! $upgrade ]]; then
remove_cached_plaintext
fi
# remove helper scripts # remove helper scripts
# Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade for script in {clean,smudge,textconv}; do
for script in {transcrypt,clean,smudge,textconv,merge}; do [[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
[[ ! -f "${CRYPT_DIR}/${script}" ]] || rm "${CRYPT_DIR}/${script}"
done done
[[ ! -d "${CRYPT_DIR}" ]] || rmdir "${CRYPT_DIR}" [[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"
# rename helper hooks (don't delete, in case user has custom changes)
pre_commit_hook="${GIT_HOOKS}/pre-commit"
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
if [[ -f "$pre_commit_hook" ]]; then
hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook")
installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed")
if [[ "$hook_md5" = "$installed_md5" ]]; then
rm "$pre_commit_hook"
else
printf 'WARNING: Cannot safely disable Git pre-commit hook %s please check it yourself\n' "$pre_commit_hook"
fi
fi
[[ -f "$pre_commit_hook_installed" ]] && rm "$pre_commit_hook_installed"
# touch all encrypted files to prevent stale stat info # touch all encrypted files to prevent stale stat info
local encrypted_files local encrypted_files
@ -696,85 +503,23 @@ uninstall_transcrypt() {
case $OSTYPE in case $OSTYPE in
darwin*) darwin*)
/usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES" /usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
/usr/bin/sed -i '' '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;; ;;
linux*) linux*)
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES" sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
sed -i '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;; ;;
esac esac
if [[ ! $upgrade ]]; then printf 'The transcrypt configuration has been completely removed from the repository.\n'
printf 'The transcrypt configuration has been completely removed from the repository.\n'
fi
else else
die 1 'uninstallation has been aborted' die 1 'uninstallation has been aborted'
fi fi
} }
# uninstall and re-install transcrypt to upgrade scripts and update configuration
upgrade_transcrypt() {
CURRENT_VERSION=$(git config --get --local transcrypt.version 2>/dev/null)
if [[ $interactive ]]; then
printf 'You are about to upgrade the transcrypt scripts in your repository.\n'
printf 'Your configuration settings will not be changed.\n\n'
printf ' Current version: %s\n' "$CURRENT_VERSION"
printf 'Upgraded version: %s\n\n' "$VERSION"
printf 'Proceed with upgrade? [y/N] '
read -r answer
printf '\n'
if [[ $answer =~ $YES_REGEX ]]; then
# User confirmed, don't prompt again
interactive=''
else
# User did not confirm, exit
# Exit if user did not confirm
die 1 'upgrade has been aborted'
fi
fi
# Keep current cipher and password
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
# Keep current openssl-path, or set to default if no existing value
openssl_path=$(git config --get --local transcrypt.openssl-path 2>/dev/null || printf '%s' "$openssl_path")
# Keep contents of .gitattributes
ORIG_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
uninstall_transcrypt
save_configuration
# Re-instate contents of .gitattributes
echo "$ORIG_GITATTRIBUTES" >"$GIT_ATTRIBUTES"
# Update .gitattributes for transcrypt'ed files to include "merge=crypt" config
case $OSTYPE in
darwin*)
/usr/bin/sed -i '' 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
;;
linux*)
sed -i 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
;;
esac
printf 'Upgrade is complete\n'
LATEST_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
if [[ "$LATEST_GITATTRIBUTES" != "$ORIG_GITATTRIBUTES" ]]; then
printf '\nYour gitattributes file has been updated with the latest recommended values.\n'
printf 'Please review and commit the new values in:\n'
printf '%s\n' "$GIT_ATTRIBUTES"
fi
}
# list all of the currently encrypted files in the repository # list all of the currently encrypted files in the repository
list_files() { list_files() {
if [[ $IS_BARE == 'false' ]]; then if [[ $IS_BARE == 'false' ]]; then
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO" cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }' git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
fi fi
} }
@ -783,8 +528,8 @@ show_raw_file() {
if [[ -f $show_file ]]; then if [[ -f $show_file ]]; then
# ensure the file is currently being tracked # ensure the file is currently being tracked
local escaped_file=${show_file//\//\\\/} local escaped_file=${show_file//\//\\\/}
if git -c core.quotePath=false ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then if git ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
file_paths=$(git -c core.quotePath=false ls-tree --name-only --full-name HEAD "$show_file") file_paths=$(git ls-tree --name-only --full-name HEAD "$show_file")
else else
die 1 'the file "%s" is not currently being tracked by git' "$show_file" die 1 'the file "%s" is not currently being tracked by git' "$show_file"
fi fi
@ -817,10 +562,10 @@ export_gpg() {
current_cipher=$(git config --get --local transcrypt.cipher) current_cipher=$(git config --get --local transcrypt.cipher)
local current_password local current_password
current_password=$(git config --get --local transcrypt.password) current_password=$(git config --get --local transcrypt.password)
mkdir -p "${CRYPT_DIR}" mkdir -p "${GIT_DIR}/crypt"
local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -" local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -"
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${CRYPT_DIR}/${gpg_recipient}.asc" printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc"
printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient" printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
} }
@ -830,10 +575,10 @@ import_gpg() {
command -v gpg >/dev/null || die 'required command "gpg" was not found' command -v gpg >/dev/null || die 'required command "gpg" was not found'
local path local path
if [[ -f "${CRYPT_DIR}/${gpg_import_file}" ]]; then if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then
path="${CRYPT_DIR}/${gpg_import_file}" path="${GIT_DIR}/crypt/${gpg_import_file}"
elif [[ -f "${CRYPT_DIR}/${gpg_import_file}.asc" ]]; then elif [[ -f "${GIT_DIR}/crypt/${gpg_import_file}.asc" ]]; then
path="${CRYPT_DIR}/${gpg_import_file}.asc" path="${GIT_DIR}/crypt/${gpg_import_file}.asc"
elif [[ ! -f $gpg_import_file ]]; then elif [[ ! -f $gpg_import_file ]]; then
die 1 'the file "%s" does not exist' "$gpg_import_file" die 1 'the file "%s" does not exist' "$gpg_import_file"
else else
@ -891,9 +636,6 @@ help() {
the password to derive the key from; the password to derive the key from;
defaults to 30 random base64 characters defaults to 30 random base64 characters
--set-openssl-path=PATH_TO_OPENSSL
use OpenSSL at this path; defaults to 'openssl' in \$PATH
-y, --yes -y, --yes
assume yes and accept defaults for non-specified options assume yes and accept defaults for non-specified options
@ -915,10 +657,6 @@ help() {
remove all transcrypt configuration from the repository and remove all transcrypt configuration from the repository and
leave files in the current working copy decrypted leave files in the current working copy decrypted
--upgrade
apply the latest transcrypt scripts in the repository without
changing your configuration settings
-l, --list -l, --list
list all of the transparently encrypted files in the repository, list all of the transparently encrypted files in the repository,
relative to the top-level directory relative to the top-level directory
@ -951,12 +689,12 @@ help() {
$ transcrypt $ transcrypt
Once a repository has been configured with transcrypt, you can trans- Once a repository has been configured with transcrypt, you can trans-
parently encrypt files by applying the "crypt" filter, diff and merge parently encrypt files by applying the "crypt" filter and diff to a
to a pattern in the top-level .gitattributes config. If that pattern pattern in the top-level .gitattributes config. If that pattern matches
matches a file in your repository, the file will be transparently a file in your repository, the file will be transparently encrypted
encrypted once you stage and commit it: once you stage and commit it:
$ echo 'sensitive_file filter=crypt diff=crypt merge=crypt' >> .gitattributes $ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
$ git add .gitattributes sensitive_file $ git add .gitattributes sensitive_file
$ git commit -m 'Add encrypted version of a sensitive file' $ git commit -m 'Add encrypted version of a sensitive file'
@ -984,52 +722,23 @@ help() {
# reset all variables that might be set # reset all variables that might be set
cipher='' cipher=''
display_config=''
flush_creds=''
gpg_import_file=''
gpg_recipient=''
interactive='true'
list=''
password='' password=''
interactive='true'
display_config=''
rekey='' rekey=''
show_file='' flush_creds=''
uninstall='' uninstall=''
upgrade='' show_file=''
openssl_path='openssl' gpg_recipient=''
gpg_import_file=''
# used to bypass certain safety checks # used to bypass certain safety checks
requires_existing_config='' requires_existing_config=''
requires_clean_repo='true' requires_clean_repo='true'
ignore_config_status='' # Set for operations where config can exist or not
# parse command line options # parse command line options
while [[ "${1:-}" != '' ]]; do while [[ "${1:-}" != '' ]]; do
case $1 in case $1 in
clean)
shift
git_clean "$@"
exit $?
;;
smudge)
shift
git_smudge "$@"
exit $?
;;
textconv)
shift
git_textconv "$@"
exit $?
;;
merge)
shift
git_merge "$@"
exit $?
;;
pre_commit)
shift
git_pre_commit "$@"
exit $?
;;
-c | --cipher) -c | --cipher)
cipher=$2 cipher=$2
shift shift
@ -1044,11 +753,6 @@ while [[ "${1:-}" != '' ]]; do
--password=*) --password=*)
password=${1#*=} password=${1#*=}
;; ;;
--set-openssl-path=*)
openssl_path=${1#*=}
# Immediately apply config setting
git config transcrypt.openssl-path "$openssl_path"
;;
-y | --yes) -y | --yes)
interactive='' interactive=''
;; ;;
@ -1073,15 +777,9 @@ while [[ "${1:-}" != '' ]]; do
requires_existing_config='true' requires_existing_config='true'
requires_clean_repo='' requires_clean_repo=''
;; ;;
--upgrade)
upgrade='true'
requires_existing_config='true'
requires_clean_repo=''
;;
-l | --list) -l | --list)
list='true' list_files
requires_clean_repo='' exit 0
ignore_config_status='true'
;; ;;
-s | --show-raw) -s | --show-raw)
show_file=$2 show_file=$2
@ -1133,25 +831,14 @@ while [[ "${1:-}" != '' ]]; do
shift shift
done done
gather_repo_metadata
# always run our safety checks # always run our safety checks
run_safety_checks run_safety_checks
# regular expression used to test user input
readonly YES_REGEX='^[Yy]$'
# in order to keep behavior consistent no matter what order the options were # in order to keep behavior consistent no matter what order the options were
# specified in, we must run these here rather than in the case statement above # specified in, we must run these here rather than in the case statement above
if [[ $list ]]; then if [[ $uninstall ]]; then
list_files
exit 0
elif [[ $uninstall ]]; then
uninstall_transcrypt uninstall_transcrypt
exit 0 exit 0
elif [[ $upgrade ]]; then
upgrade_transcrypt
exit 0
elif [[ $display_config ]] && [[ $flush_creds ]]; then elif [[ $display_config ]] && [[ $flush_creds ]]; then
display_configuration display_configuration
printf '\n' printf '\n'
@ -1193,7 +880,7 @@ fi
# ensure the git attributes file exists # ensure the git attributes file exists
if [[ ! -f $GIT_ATTRIBUTES ]]; then if [[ ! -f $GIT_ATTRIBUTES ]]; then
mkdir -p "${GIT_ATTRIBUTES%/*}" mkdir -p "${GIT_ATTRIBUTES%/*}"
printf '#pattern filter=crypt diff=crypt merge=crypt\n' >"$GIT_ATTRIBUTES" printf '#pattern filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES"
fi fi
printf 'The repository has been successfully configured by transcrypt.\n' printf 'The repository has been successfully configured by transcrypt.\n'

View File

@ -1,4 +1,8 @@
{ pkgs, lib, ... }: { lib, ... }:
let
secrets = toString ./secrets;
in
{ {
imports = [ imports = [
@ -6,25 +10,18 @@
./configuration.nix ./configuration.nix
]; ];
# VM hardware setup
virtualisation.memorySize = 4000; # MB
virtualisation.graphics = false;
virtualisation.cores = 4;
virtualisation.msize = 1 * 1024 * 1024;
# Ensure secrets are accessible by the # Ensure secrets are accessible by the
# activation scripts at runtime. # activation scripts at runtime.
virtualisation.sharedDirectories.secrets = virtualisation.qemu.options = [
{ source = toString ./secrets; "-virtfs local,path=${secrets},security_model=none,mount_tag=secrets"
target = toString ./secrets; ];
}; fileSystems = lib.mkVMOverride {
"${secrets}" =
# These don't work in a virtual machine { device = "secrets";
systemd.services.smartd.enable = lib.mkForce false; fsType = "9p";
systemd.services.apcupsd.enable = lib.mkForce false; options = [ "trans=virtio" "version=9p2000.L" ];
neededForBoot = true;
# Automatically resize the console };
environment.systemPackages = [ pkgs.xterm ]; };
environment.shellInit = "resize > /dev/null";
} }

View File

@ -9,10 +9,8 @@
type = lib.types.attrs; type = lib.types.attrs;
readOnly = true; readOnly = true;
default = { default = {
hostname = "maxwell.eurofusion.eu"; hostname = "maxwell.ydns.eu";
ipv4WanAddress = "2.35.5.112"; ipAddress = "2.25.5.112";
ipv4LanAddress = "192.168.1.5";
ipv6Address = "2001:470:b576:0:230:48ff:fefa:91e1";
}; };
description = "Global constants."; description = "Global constants.";
}; };