diff --git a/README.md b/README.md index 89f3c49..85e7857 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ environment variables that can be passed to sops. - Atomic upgrades: New secrets are written to a new directory which replaces the old directory atomically. - Rollback support: If sops files are added to the Nix store, old secrets can be rolled back. This is optional. - Fast time-to-deploy: Unlike solutions implemented by NixOps, krops and morph, no extra steps are required to upload secrets. -- A variety of storage formats: Secrets can be stored in YAML, JSON or binary. +- A variety of storage formats: Secrets can be stored in YAML, dotenv, INI, JSON or binary. - Minimizes configuration errors: sops files are checked against the configuration at evaluation time. ## Demo @@ -603,7 +603,7 @@ As users are not created yet, it's not possible to set an owner for these secret ## Different file formats -At the moment we support the following file formats: YAML, JSON, and binary. +At the moment we support the following file formats: YAML, JSON, INI, dotenv and binary. sops-nix allows specifying multiple sops files in different file formats: diff --git a/default.nix b/default.nix index 97c35e2..6797d84 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,5 @@ { pkgs ? import {} }: let - vendorSha256 = "sha256-7DHISMAs7i6Yow0/ORiC8gLLfFeuSeiP8QchnJe3M+M="; + vendorSha256 = "sha256-ZAHshOBbvwOC1618bi7IqOan+YtDT7DJNsLzV/4OBSg="; buildGoModule = if pkgs.lib.versionOlder pkgs.go.version "1.18" then pkgs.buildGo118Module else pkgs.buildGoModule; sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { diff --git a/go.mod b/go.mod index c611f4e..7860aaf 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/vault/api v1.7.2 // indirect github.com/hashicorp/vault/sdk v0.5.2 // indirect + github.com/joho/godotenv v1.4.0 // indirect github.com/lib/pq v1.10.6 // indirect github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 go.mozilla.org/sops/v3 v3.7.3 diff --git a/go.sum b/go.sum index 9eec0d9..dc0e281 100644 --- a/go.sum +++ b/go.sum @@ -403,6 +403,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -758,7 +760,6 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/modules/sops/default.nix b/modules/sops/default.nix index f595ba5..2aac71e 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -41,7 +41,7 @@ let ''; }; format = mkOption { - type = types.enum ["yaml" "json" "binary"]; + type = types.enum ["yaml" "json" "binary" "dotenv" "ini"]; default = cfg.defaultSopsFormat; description = '' File format used to decrypt the sops secret. diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 7b23c3e..12d5609 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -24,6 +24,7 @@ import ( "github.com/mozilla-services/yaml" "go.mozilla.org/sops/v3/decrypt" "golang.org/x/sys/unix" + "github.com/joho/godotenv" ) type secret struct { @@ -73,8 +74,23 @@ const ( Yaml FormatType = "yaml" Json FormatType = "json" Binary FormatType = "binary" + Dotenv FormatType = "dotenv" + Ini FormatType = "ini" ) +func IsValidFormat(format string) bool { + switch format { + case string(Yaml), + string(Json), + string(Binary), + string(Dotenv), + string(Ini): + return true + default: + return false + } +} + func (f *FormatType) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { @@ -84,7 +100,7 @@ func (f *FormatType) UnmarshalJSON(b []byte) error { switch t { case "": *f = Yaml - case Yaml, Json, Binary: + case Yaml, Json, Binary, Dotenv, Ini: *f = t } @@ -270,23 +286,26 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { if err != nil { return fmt.Errorf("Failed to decrypt '%s': %w", s.SopsFile, err) } - if s.Format == Binary { + + switch s.Format { + case Binary, Dotenv, Ini: sourceFile.binary = plain - } else { - if s.Format == Yaml { - if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err) - } - } else { - if err := json.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) - } + case Yaml: + if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { + return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err) } + case Json: + if err := json.Unmarshal(plain, &sourceFile.data); err != nil { + return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + } + default: + return fmt.Errorf("Secret of type %s in %s is not supported", s.Format, s.SopsFile) } } - if s.Format == Binary { + switch s.Format { + case Binary, Dotenv, Ini: s.value = sourceFile.binary - } else { + case Yaml, Json: strVal, err := recurseSecretKey(sourceFile.data, s.Key) if err != nil { return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) @@ -410,19 +429,30 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { } var keys map[string]interface{} - if s.Format == Binary { + + switch s.Format { + case Binary: if err := json.Unmarshal(cipherText, &keys); err != nil { return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) } return &secretFile{cipherText: cipherText, firstSecret: s}, nil - } - - if s.Format == Yaml { + case Yaml: if err := yaml.Unmarshal(cipherText, &keys); err != nil { return nil, fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err) } - } else if err := json.Unmarshal(cipherText, &keys); err != nil { - return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + case Dotenv: + env, err := godotenv.Unmarshal(string(cipherText)) + if err != nil { + return nil, fmt.Errorf("Cannot parse dotenv of '%s': %w", s.SopsFile, err) + } + keys = map[string]interface{}{} + for k, v := range env { + keys[k] = v + } + case Json: + if err := json.Unmarshal(cipherText, &keys); err != nil { + return nil, fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + } } return &secretFile{ @@ -439,7 +469,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { s.Name, s.SopsFile, s.Format, file.firstSecret.Format, file.firstSecret.Name) } - if app.checkMode != Manifest && s.Format != Binary { + if app.checkMode != Manifest && (!(s.Format == Binary || s.Format == Dotenv || s.Format == Ini )) { _, err := recurseSecretKey(file.keys, s.Key) if err != nil { return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) @@ -485,7 +515,7 @@ func (app *appContext) validateSecret(secret *secret) error { secret.Format = "yaml" } - if secret.Format != "yaml" && secret.Format != "json" && secret.Format != "binary" { + if !IsValidFormat(string(secret.Format)) { return fmt.Errorf("Unsupported format %s for secret %s", secret.Format, secret.Name) } diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 095d576..c5a411e 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -14,6 +14,7 @@ import ( "reflect" "runtime" "strconv" + "strings" "syscall" "testing" ) @@ -110,7 +111,7 @@ func testGPG(t *testing.T) { ReloadUnits: []string{"affected-reload-service"}, } - var jsonSecret, binarySecret secret + var jsonSecret, binarySecret, dotenvSecret, iniSecret secret // should not create a symlink jsonSecret = yamlSecret jsonSecret.Name = "test2" @@ -127,8 +128,25 @@ func testGPG(t *testing.T) { binarySecret.SopsFile = path.Join(assets, "secrets.bin") binarySecret.Path = path.Join(testdir.secretsPath, "test3") + dotenvSecret = yamlSecret + dotenvSecret.Name = "test4" + dotenvSecret.Owner = "root" + dotenvSecret.Group = "root" + dotenvSecret.Format = "dotenv" + dotenvSecret.SopsFile = path.Join(assets, "secrets.env") + dotenvSecret.Path = path.Join(testdir.secretsPath, "test4") + + iniSecret = yamlSecret + iniSecret.Name = "test5" + iniSecret.Owner = "root" + iniSecret.Group = "root" + iniSecret.Format = "ini" + iniSecret.SopsFile = path.Join(assets, "secrets.ini") + iniSecret.Path = path.Join(testdir.secretsPath, "test5") + + manifest := manifest{ - Secrets: []secret{yamlSecret, jsonSecret, binarySecret}, + Secrets: []secret{yamlSecret, jsonSecret, binarySecret, dotenvSecret, iniSecret}, SecretsMountPoint: testdir.secretsPath, SymlinkPath: testdir.symlinkPath, GnupgHome: gpgHome, @@ -321,3 +339,17 @@ func TestValidateManifest(t *testing.T) { ok(t, installSecrets([]string{"sops-install-secrets", "-check-mode=manifest", path})) ok(t, installSecrets([]string{"sops-install-secrets", "-check-mode=sopsfile", path})) } + +func TestIsValidFormat(t *testing.T) { + generateCase := func(input string, mustBe bool) { + result := IsValidFormat(input) + if result != mustBe { + t.Errorf("input %s must return %v but returned %v", input, mustBe, result) + } + } + for _, format := range []string{string(Yaml), string(Json), string(Binary), string(Dotenv)} { + generateCase(format, true) + generateCase(strings.Title(format), false) + generateCase(strings.ToUpper(format), false) + } +} diff --git a/pkgs/sops-install-secrets/test-assets/secrets.env b/pkgs/sops-install-secrets/test-assets/secrets.env new file mode 100644 index 0000000..bf27710 --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/secrets.env @@ -0,0 +1,15 @@ +#ENC[AES256_GCM,data:TfdJsqJ9p/3tnClpyPQbfvbmYUmjryiSGA==,iv:YXiEYlAdzco3hZ7T+X6dOUb17ByZeyGXlimfD+yaTa0=,tag:67TgtX4Zn6Ft7ww+J5AjTQ==,type:comment] +hello=ENC[AES256_GCM,data:Y8BU+riqE9DInBi1iALx8iNG2Z5iAGFOgNhZg/wFtwqcLXFBTP1vouIewDdWfQ==,iv:dPftz1CxGYi81lSUbg0iRhXpFP4blmyRn5qKPnWUF0k=,tag:VzKiYYhNDSJsZ8q1A+EpMg==,type:str] +example_key=ENC[AES256_GCM,data:C8+6JSN7MbRpizcF9A==,iv:ODsEI46iuAT81Q/8r83tCfKpU9x2zJ3rzV4FhJmj+Xs=,tag:Mc4l4Kvzg0VvmzcvU3w1tA==,type:str] +example_multiline=ENC[AES256_GCM,data:M6QISEHpqpyUVC0=,iv:uow+EKFgOuSv84hCqtox4r8nvVRFC11xG7or7iMdNkg=,tag:AC5aoWP1LgALV1/YPZMplg==,type:str] +never_gonna=ENC[AES256_GCM,data:PrjZGWkgmmrT6Ms=,iv:ZaSHmgdKf0EUFehl+z4Xj2ouiw6T17xhqwsCGP8fdgQ=,tag:5YRX6obm4G9RUQIqECMf5A==,type:str] +sops_pgp__list_1__map_enc=-----BEGIN PGP MESSAGE-----\n\nhQGMA3ulPRkZxd/UAQwAtsL/Gdj32m81J5k3q4Vz+ev5B+zF/53hB+3FuJtUlWez\naNxQs6RxGC1JtlluMX0syzz8yoAspnfbKxPylMf/A81dhqnMVpZyktBtavb6K07E\nl2gOjgkq6SfOzqeVQdxjvi4VoZ86+KueQCPlALQWxVMGlCMjhGwd7HLWFbJt6O2G\nNCNa7HBABYUDQf8lND+7YtBKX4KyxJviQGpdlOvx7Xkw7hafYMxA79Jp7uLso5d4\n+IKhidKqR2ZWhNgcgKCesRS4hIAzqMmBiSNYHuM23Pe+jPzOvbADWPJEj/rV3qvo\ni8ehS/cGB4rRnLdfW6frQpC+cVKhxuZTJmoBXFazIB15fz32iuDZ8FBiJJXYrVJT\nIphOJt9NcnpoK4M5mQvKB6otZfZRfQYjbJ4Q7n5u4AD/TvMByRgG2B7XkaEXRbrx\ngNuSpaLtwTYpqzq/AynR3h4K5D0izhoviIrhyv6Y5EskDim5ulqbcW+lPAISkohE\nhqWx76/SyA0ehj9jqTrW0lgBXQDsnqgS5C0LZFLsPyDvEG0q03/eoUKSbYyVU5kh\nwy8yHcJmsDqgU+f5TjyNdXpOc+xfbGqfWmeWjSPDyx7lEZxXcBXqAVCpJo3QL9JX\n+VDTtrwqJ2Ho\n=nthm\n-----END PGP MESSAGE-----\n +sops_mac=ENC[AES256_GCM,data:onO42qeKMXa4Jl6Vt06zaoET+6WKzSGd8ak/LyXq1xABAtbVpt25tBdVKlfCD/lzDkOylexQfOBdRAqRPZ1ex3Q4ILZcjmPBkFsMbAs/7L1EzSXskWgeh7NuDXtNKsTmGp9BVcICh3O1+kECQSRZGcefUhV+HYMjRxh2Xxm8r5E=,iv:C8m3Ffbe12LSv0bsPmsxfhOjXNoJ0+75Q+sCYpqchJA=,tag:pahCk/P1Ig71xgvxVIZyPg==,type:str] +sops_version=3.7.3 +sops_unencrypted_suffix=_unencrypted +sops_pgp__list_1__map_fp=2504791468B153B8A3963CC97BA53D1919C5DFD4 +sops_pgp__list_0__map_fp=7FB89715AADA920D65D25E63F9BA9DEBD03F57C0 +sops_pgp__list_0__map_created_at=2023-01-17T12:32:47Z +sops_lastmodified=2023-01-17T12:33:06Z +sops_pgp__list_0__map_enc=-----BEGIN PGP MESSAGE-----\n\nhQEMA/m6nevQP1fAAQf/cGIbOAVh6dy2xDztWHmOPfhBsEFJRzih25cVNbVqo6EC\nxAY31eEZpibKDhAxNKSQbUXjwpCY2Bw5iyRznvjy2kuwDxjyqbGVsNKJuLvlqZ/f\n8Pfs5xvI0A3nc4PRwm3U8n0UhrII9zMl9VB2THw7CP5ZnJy0mjEygxI7ml7k63Go\nSAukABD6QW1sIluP2Q7A6Cy7nXf8QcXI0O5cMJbQos8OOEIiRWoD33i5Uf9KNh9c\nwhNNvpfh1cMZ5StlaWlNXW3ZH/pOJWCLnmmQ/DgcR+LiMA01moykE9ewtwkXfwED\nRzNYZhFD2NKn9Y+smUQ2XaXwMeBw7wlCY7568wK3wtJeAVAEJHbFS7CXI1x3zRRL\nkEy3AI85MobyjGdWIi0+v1K3TqbABUNyg9O+6XpSkn3zPtneY1w4EgJqEvMMJoxN\nxGahyyxYhT2VDO7gPrlkITE6mo0jwyfCGjZERpQFiw==\n=X2R+\n-----END PGP MESSAGE-----\n +sops_pgp__list_1__map_created_at=2023-01-17T12:32:47Z diff --git a/pkgs/sops-install-secrets/test-assets/secrets.ini b/pkgs/sops-install-secrets/test-assets/secrets.ini new file mode 100644 index 0000000..51fe83f --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/secrets.ini @@ -0,0 +1,18 @@ +; ENC[AES256_GCM,data:Q3CfbslIuolYPK9yZIgPdgnmYSuwBG9E,iv:d51C4MXhAa0pOMSTDtSzNyxgRd3IkHPW4+tCyTpHxbY=,tag:LzXKrOg72Td9PAhb4UdKMA==,type:comment] +[Welcome!] +hello = ENC[AES256_GCM,data:NjaXxfkFK25JFGiPNJBDX0NfsZN70ltV4OCLR52PCFU880dw5NU8O2yvQQ2kUQ==,iv:eSYE2Pwu8J7Wdq0t80Dx1OKfq8B5nCkX8FqFnVqOSSM=,tag:gqf3oczxJM4V9/L48/PFAg==,type:str] +example_key = ENC[AES256_GCM,data:aYmYszjMLR+eEW0ZZw==,iv:K6qCGzVu56gjUWXtEad7QYRh/6OpytdkoyzOjp/1lMw=,tag:HqMQ0ckO7uVyOiZbOtbxkA==,type:str] +never_gonna = ENC[AES256_GCM,data:Q4xcNZE1R6tiXh4=,iv:7BgJXn3avu+7Mc9FRPqW0VtRiK7nUpA6cHfhzEWV+TA=,tag:/4YjMUA2la3/E71lgNcqIw==,type:str] + +[sops] +pgp__list_0__map_created_at = 2023-01-17T13:06:50Z +mac = ENC[AES256_GCM,data:jCYesEFva/ptI23sBcOHxMhGsyF9E5w3tdsEONKtJj+7KbkG76f7e3AEQyjpURNuv6QVHwCwABJRJObl4VHXOoI/yb/AKSGSYPTM/nRYWXG8vrX1HHwFYWmQfZtm6G8bc8bhWjHh3nt6cV63VhfNB+5L3oaTdkrKfZhNrLI5ztI=,iv:VSbopP0E+ocMbsaM6jwkiG4K/H6N5JUv+3kKtU2jI6Q=,tag:TTjz2JifWiNSs8sComn5Ew==,type:str] +version = 3.7.3 +pgp__list_0__map_enc = -----BEGIN PGP MESSAGE-----\n\nhQEMA/m6nevQP1fAAQf/S9ggtaO8grsEYyicoQnrM0279m0+t5d6VP2bQoelneOw\nPEZOdKnwWw939rdUz5pNMWIdEL2mHE1Yu99lmal9TmMXf0kDoryDgHtChM6UyA/u\n0mqbEoASaebwAikWJcidhoWbTLr0eqpohVr+Y0wT3MjkX4sUZzPRapIT2rv/jecm\nq357aSy0kzmKnIal+h9Bqbl3tkiMvhdILRsN1Xlp7xa3H681D1EeRfw3BbR8h4Ui\ng9m9kBVRVmyLl46C4a1etcQgvU5jFUloDIaVV3XGErdEz8bL7B5jPuH5xJCAmVOM\nrouo1n6xJGxEq1lUoWTf/wfnsCCJzpPktsikeprj+9JeAT5DsZdnHr3nmgNYEgLp\neUTTB4aoKHsjehHV+aTpJSKPtzkAkA4mEregUes4T20WOYgurg2lf9a1TYQ+W5ve\ngrI2jdYYxVkWRJvuZRW3umKKQZhx96AP7RczbWL5BA==\n=WuNZ\n-----END PGP MESSAGE-----\n +lastmodified = 2023-01-17T13:07:01Z +unencrypted_suffix = _unencrypted +pgp__list_1__map_created_at = 2023-01-17T13:06:50Z +pgp__list_1__map_enc = -----BEGIN PGP MESSAGE-----\n\nhQGMA3ulPRkZxd/UAQv9FFWTbfSQoC6OVhfIEk5+6t35rAAaJAEGyYPLDqRu0xQk\nd80jcsCmvFo8NqKQfBsC6GTsvbnAOuErIYcltKDya0mULDgskbCDQmrwF6AL0Bp9\noVnJ60tuMc72yGYgKTu7yll2DUJuas/qltvI/aA7SMFltltIqnPv7byZfH3BAJIY\nVpnNO1a9M1S7YrS8GtuLSdXfUWqpzoE2bVhsJCfQy4yxkyyMsEnObb6xTTSD2iio\nZXU50fR8JqXma1Z2XuVUmWLS7mp03iqIzwrBCIVuhfSYmy3gF36rToJ5EFgEhP6f\nB8S4xZyWN9Pp6r3keaKK4cYybKrk9rDUb1JcKiP0K/lGsX52M+IR0x0peTVp0ciq\nfYKOd9leEI8nsBpeMuvQnKhE+XHWiQBghmUCeK35O9oYJGKLrsKVXfMke7LUE5Zm\nQQ1CjvvVovAnuX/8/1+NscixBgXZXZgTqoRE84Qta+ohRZqmNGIv2VeEZx/jQxXG\nhiA5G+ylnlTZPGck8V5D0lgB++OQoiZVryFH1aVr1AHO0j2lCa3ckftLKkQ5vQIi\n1JHbPHZ1MHBut761L/RSnBmTVr3B3XW54NH78LeWS0y7z+8mXjWy7Cjl4cyst1b0\n/byCrk2EqPQP\n=LZBU\n-----END PGP MESSAGE-----\n +pgp__list_1__map_fp = 2504791468B153B8A3963CC97BA53D1919C5DFD4 +pgp__list_0__map_fp = 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0 +