From e0cb8a9187bfa7545f5fbb6885058dd9ebf159bd Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 3 Feb 2024 22:55:49 +0100 Subject: [PATCH] Vendor out the keybase pinentry --- agent/systemauth/pinentry/keybase-pinentry.go | 2 +- .../pinentry/keybase-pinentry/LICENSE.md | 27 +++ .../pinentry/keybase-pinentry/pinentry.go | 219 ++++++++++++++++++ .../pinentry/keybase-pinentry/pinentry_nix.go | 111 +++++++++ .../keybase-pinentry/pinentry_not_osx.go | 17 ++ .../pinentry/keybase-pinentry/pinentry_osx.go | 117 ++++++++++ .../keybase-pinentry/pinentry_windows.go | 112 +++++++++ .../pinentry/keybase-pinentry/pinentry_x11.go | 14 ++ go.mod | 3 +- go.sum | 2 + 10 files changed, 622 insertions(+), 2 deletions(-) create mode 100644 agent/systemauth/pinentry/keybase-pinentry/LICENSE.md create mode 100644 agent/systemauth/pinentry/keybase-pinentry/pinentry.go create mode 100644 agent/systemauth/pinentry/keybase-pinentry/pinentry_nix.go create mode 100644 agent/systemauth/pinentry/keybase-pinentry/pinentry_not_osx.go create mode 100644 agent/systemauth/pinentry/keybase-pinentry/pinentry_osx.go create mode 100644 agent/systemauth/pinentry/keybase-pinentry/pinentry_windows.go create mode 100644 agent/systemauth/pinentry/keybase-pinentry/pinentry_x11.go diff --git a/agent/systemauth/pinentry/keybase-pinentry.go b/agent/systemauth/pinentry/keybase-pinentry.go index ec7dfe3..605016f 100644 --- a/agent/systemauth/pinentry/keybase-pinentry.go +++ b/agent/systemauth/pinentry/keybase-pinentry.go @@ -6,8 +6,8 @@ import ( "errors" "github.com/keybase/client/go/logger" - "github.com/keybase/client/go/pinentry" "github.com/keybase/client/go/protocol/keybase1" + pinentry "github.com/quexten/goldwarden/agent/systemauth/pinentry/keybase-pinentry" ) func GetPassword(title string, description string) (string, error) { diff --git a/agent/systemauth/pinentry/keybase-pinentry/LICENSE.md b/agent/systemauth/pinentry/keybase-pinentry/LICENSE.md new file mode 100644 index 0000000..5f78369 --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/LICENSE.md @@ -0,0 +1,27 @@ +Copyright (c) 2015, Keybase +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of keybase nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/agent/systemauth/pinentry/keybase-pinentry/pinentry.go b/agent/systemauth/pinentry/keybase-pinentry/pinentry.go new file mode 100644 index 0000000..4d871eb --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/pinentry.go @@ -0,0 +1,219 @@ +// Copyright 2015 Keybase, Inc. All rights reserved. Use of +// this source code is governed by the included BSD license. + +package pinentry + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/keybase/client/go/logger" + keybase1 "github.com/keybase/client/go/protocol/keybase1" +) + +// +// some borrowed from here: +// +// https://github.com/bradfitz/camlistore/blob/master/pkg/misc/pinentry/pinentry.go +// +// Under the Apache 2.0 license +// + +type Pinentry struct { + initRes *error + path string + term string + tty string + prog string + log logger.Logger +} + +func New(envprog string, log logger.Logger, tty string) *Pinentry { + return &Pinentry{ + prog: envprog, + log: log, + tty: tty, + } +} + +func (pe *Pinentry) Init() (error, error) { + if pe.initRes != nil { + return *pe.initRes, nil + } + err, fatalerr := pe.FindProgram() + if err == nil { + pe.GetTerminalName() + } + pe.term = os.Getenv("TERM") + pe.initRes = &err + return err, fatalerr +} + +func (pe *Pinentry) SetInitError(e error) { + pe.initRes = &e +} + +func (pe *Pinentry) FindProgram() (error, error) { + prog := pe.prog + var err, fatalerr error + if len(prog) > 0 { + if err = canExec(prog); err == nil { + pe.path = prog + } else { + err = fmt.Errorf("Can't execute given pinentry program '%s': %s", + prog, err) + fatalerr = err + } + } else if prog, err = FindPinentry(pe.log); err == nil { + pe.path = prog + } + return err, fatalerr +} + +func (pe *Pinentry) Get(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) { + + pe.log.Debug("+ Pinentry::Get()") + + // Do a lazy initialization + if err, _ = pe.Init(); err != nil { + return + } + + inst := pinentryInstance{parent: pe} + defer inst.Close() + + if err = inst.Init(); err != nil { + // We probably shouldn't try to use this thing again if we failed + // to set it up. + pe.SetInitError(err) + return + } + res, err = inst.Run(arg) + pe.log.Debug("- Pinentry::Get() -> %v", err) + return +} + +func (pi *pinentryInstance) Close() { + pi.stdin.Close() + pi.cmd.Wait() +} + +type pinentryInstance struct { + parent *Pinentry + cmd *exec.Cmd + stdout io.ReadCloser + stdin io.WriteCloser + br *bufio.Reader +} + +func (pi *pinentryInstance) Set(cmd, val string, errp *error) { + if val == "" { + return + } + fmt.Fprintf(pi.stdin, "%s %s\n", cmd, val) + line, _, err := pi.br.ReadLine() + if err != nil { + *errp = err + return + } + if string(line) != "OK" { + *errp = fmt.Errorf("Response to " + cmd + " was " + string(line)) + } + return +} + +func (pi *pinentryInstance) Init() (err error) { + parent := pi.parent + + parent.log.Debug("+ pinentryInstance::Init()") + + pi.cmd = exec.Command(parent.path) + pi.stdin, _ = pi.cmd.StdinPipe() + pi.stdout, _ = pi.cmd.StdoutPipe() + + if err = pi.cmd.Start(); err != nil { + parent.log.Warning("unexpected error running pinentry (%s): %s", parent.path, err) + return + } + + pi.br = bufio.NewReader(pi.stdout) + lineb, _, err := pi.br.ReadLine() + + if err != nil { + err = fmt.Errorf("Failed to get getpin greeting: %s", err) + return + } + + line := string(lineb) + if !strings.HasPrefix(line, "OK") { + err = fmt.Errorf("getpin greeting didn't say 'OK', said: %q", line) + return + } + + if len(parent.tty) > 0 { + parent.log.Debug("setting ttyname to %s", parent.tty) + pi.Set("OPTION", "ttyname="+parent.tty, &err) + if err != nil { + parent.log.Debug("error setting ttyname: %s", err) + } + } + if len(parent.term) > 0 { + parent.log.Debug("setting ttytype to %s", parent.term) + pi.Set("OPTION", "ttytype="+parent.term, &err) + if err != nil { + parent.log.Debug("error setting ttytype: %s", err) + } + } + + parent.log.Debug("- pinentryInstance::Init() -> %v", err) + return +} + +func descEncode(s string) string { + s = strings.Replace(s, "%", "%%", -1) + s = strings.Replace(s, "\n", "%0A", -1) + return s +} + +func resDecode(s string) string { + s = strings.Replace(s, "%25", "%", -1) + return s +} + +func (pi *pinentryInstance) Run(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) { + + pi.Set("SETPROMPT", arg.Prompt, &err) + pi.Set("SETDESC", descEncode(arg.Desc), &err) + pi.Set("SETOK", arg.Ok, &err) + pi.Set("SETCANCEL", arg.Cancel, &err) + pi.Set("SETERROR", arg.Err, &err) + + if err != nil { + return + } + + fmt.Fprintf(pi.stdin, "GETPIN\n") + var lineb []byte + lineb, _, err = pi.br.ReadLine() + if err != nil { + err = fmt.Errorf("Failed to read line after GETPIN: %v", err) + return + } + line := string(lineb) + switch { + case strings.HasPrefix(line, "D "): + res = &keybase1.SecretEntryRes{Text: resDecode(line[2:])} + case strings.HasPrefix(line, "ERR 83886179 canceled") || strings.HasPrefix(line, "ERR 83886179 Operation cancelled"): + res = &keybase1.SecretEntryRes{Canceled: true} + case line == "OK": + res = &keybase1.SecretEntryRes{} + default: + return nil, fmt.Errorf("GETPIN response didn't start with D; got %q", line) + } + + return +} diff --git a/agent/systemauth/pinentry/keybase-pinentry/pinentry_nix.go b/agent/systemauth/pinentry/keybase-pinentry/pinentry_nix.go new file mode 100644 index 0000000..30e4946 --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/pinentry_nix.go @@ -0,0 +1,111 @@ +// Copyright 2015 Keybase, Inc. All rights reserved. Use of +// this source code is governed by the included BSD license. + +//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris + +package pinentry + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/keybase/client/go/logger" +) + +// +// some borrowed from here: +// +// https://github.com/bradfitz/camlistore/blob/master/pkg/misc/pinentry/pinentry.go +// +// Under the Apache 2.0 license +// + +func canExec(s string) error { + fi, err := os.Stat(s) + if err != nil { + return err + } + mode := fi.Mode() + + // + // Only consider non-directories that have at least one +x + // bit set. + // + // TODO: Recheck this on windows! + // See here for lookpath: http://golang.org/src/pkg/os/exec/lp_windows.go + // + // Similar to check from exec.LookPath below + // See here: http://golang.org/src/pkg/os/exec/lp_unix.go + // + if mode.IsDir() { + return fmt.Errorf("Program '%s' is a directory", s) + } else if int(mode)&0111 == 0 { + return fmt.Errorf("Program '%s' isn't executable", s) + } else { + return nil + } +} + +func FindPinentry(log logger.Logger) (string, error) { + if !HasWindows() { + return "", fmt.Errorf("Can't spawn gui window, not using pinentry") + } + bins := []string{ + // If you install MacTools you'll wind up with this pinentry + "/usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac", + } + + extraPaths := []string{} + + log.Debug("+ FindPinentry()") + + cmds := []string{ + "pinentry-gtk-2", + "pinentry-qt4", + "pinentry", + } + + checkFull := func(s string) bool { + log.Debug("| Check fullpath %s", s) + found := (canExec(s) == nil) + if found { + log.Debug("- Found: %s", s) + } + return found + } + + for _, b := range bins { + if checkFull(b) { + return b, nil + } + } + + path := os.Getenv("PATH") + for _, c := range cmds { + log.Debug("| Looking for %s in standard PATH %s", c, path) + fullc, err := exec.LookPath(c) + if err == nil { + log.Debug("- Found %s", fullc) + return fullc, nil + } + } + + for _, ep := range extraPaths { + for _, c := range cmds { + full := filepath.Join(ep, c) + if checkFull(full) { + return full, nil + } + } + } + + log.Debug("- FindPinentry: none found") + return "", fmt.Errorf("No pinentry found, checked a bunch of different places") +} + +func (pe *Pinentry) GetTerminalName() { + // Noop on all platforms but windows +} diff --git a/agent/systemauth/pinentry/keybase-pinentry/pinentry_not_osx.go b/agent/systemauth/pinentry/keybase-pinentry/pinentry_not_osx.go new file mode 100644 index 0000000..8d526a4 --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/pinentry_not_osx.go @@ -0,0 +1,17 @@ +// Copyright 2015 Keybase, Inc. All rights reserved. Use of +// this source code is governed by the included BSD license. + +//go:build !darwin +// +build !darwin + +package pinentry + +type pinentrySecretStoreInfo struct{} + +func (pi *pinentryInstance) useSecretStore(useSecretStore bool) (pinentrySecretStoreInfo, error) { + return pinentrySecretStoreInfo{}, nil +} + +func (pi *pinentryInstance) shouldStoreSecret(info pinentrySecretStoreInfo) bool { + return false +} diff --git a/agent/systemauth/pinentry/keybase-pinentry/pinentry_osx.go b/agent/systemauth/pinentry/keybase-pinentry/pinentry_osx.go new file mode 100644 index 0000000..0e0a0ab --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/pinentry_osx.go @@ -0,0 +1,117 @@ +// Copyright 2015 Keybase, Inc. All rights reserved. Use of +// this source code is governed by the included BSD license. + +//go:build darwin +// +build darwin + +package pinentry + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + + "github.com/keybase/go-keychain" +) + +const ( + // pinentryServiceName is the service name that pinentry uses + // when storing into the Keychain. + pinentryServiceName = "GnuPG" + // accountNameByteLength is how many random bytes to use to + // generate the account name. 32 bytes of randomness is more + // than enough to make the account name unpredictable. + accountNameByteLength = 32 +) + +type pinentrySecretStoreInfo string + +func (pi *pinentryInstance) useSecretStore(useSecretStore bool) (pinentrySecretStoreInfo, error) { + if !useSecretStore { + return "", nil + } + + // Make account name unpredictable to make it infeasible for + // an attacker to guess (and thus sniff the passphrase). See + // https://github.com/keybase/client/issues/484#issuecomment-114313867 + // . + var accountNameBytes [accountNameByteLength]byte + n, err := rand.Read(accountNameBytes[:]) + if n != accountNameByteLength { + return "", fmt.Errorf("Unexpected random byte count %d", n) + } + if err != nil { + return "", err + } + + accountName := "keybase-" + hex.EncodeToString(accountNameBytes[:]) + + // This will cause a "Save in Keychain" checkbox to appear in + // the pinentry dialog. If checked, pinentry will then save + // the entered passphrase into the keychain with the service + // name "GnuPG" and the account name equal to the passed-in + // cache-id option value. + pi.Set("OPTION", "cache-id "+accountName, &err) + if err != nil { + // It's possible that the pinentry being used doesn't support + // this option. So just return instead of causing a fatal + // error. + pi.parent.log.Debug("| Error setting pinentry cache-id OPTION: %s", err) + pi.parent.log.Debug("| Not using secret store as a result.") + return "", nil + } + return pinentrySecretStoreInfo(accountName), err +} + +func (pi *pinentryInstance) shouldStoreSecret(info pinentrySecretStoreInfo) bool { + if len(info) == 0 { + return false + } + + // We just want to know when the user did check the "Save in + // Keychain" checkbox, so remove whatever pinentry put into + // the keychain, and infer the state of the checkbox from the + // error (since there will be no error if an entry was found + // and deleted). + // + // This is a bit of a hack -- this may cause a dialog to pop + // up saying that the client wants to access the user's + // keychain. But this will do for now until we write our own + // pinentry. + query := keychain.NewItem() + query.SetSecClass(keychain.SecClassGenericPassword) + query.SetService(pinentryServiceName) + query.SetAccount(string(info)) + query.SetMatchLimit(keychain.MatchLimitOne) + + // We need to query and delete by item reference because the + // OSX keychain API only allows us to delete unowned items + // this way. + query.SetReturnRef(true) + ref, err := keychain.QueryItemRef(query) + if err != nil { + // Default to false if there was an error. + return false + } + if ref == nil { + // If not found, return false. + return false + } + + defer keychain.Release(ref) + + err = keychain.DeleteItemRef(ref) + if err != nil { + // Default to false if there was an error deleting. + return false + } + + // Entry was found and deleted. + return true +} + +func HasWindows() bool { + // We aren't in an ssh connection, so we can probably spawn a window. + return len(os.Getenv("SSH_CONNECTION")) == 0 +} diff --git a/agent/systemauth/pinentry/keybase-pinentry/pinentry_windows.go b/agent/systemauth/pinentry/keybase-pinentry/pinentry_windows.go new file mode 100644 index 0000000..7752496 --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/pinentry_windows.go @@ -0,0 +1,112 @@ +// Copyright 2015 Keybase, Inc. All rights reserved. Use of +// this source code is governed by the included BSD license. + +//go:build windows +// +build windows + +package pinentry + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/keybase/client/go/logger" + "golang.org/x/sys/windows/registry" +) + +func HasWindows() bool { + // We're assuming you aren't using windows remotely. + return true +} + +// LookPath searches for an executable binary named file +// in the directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. + +func canExec(s string) error { + if strings.IndexAny(s, `:\/`) == -1 { + s += string(filepath.Separator) + } + _, err := exec.LookPath(s) + return err +} + +func FindPinentry(log logger.Logger) (string, error) { + + // // If you install GPG you'll wind up with this pinentry + // C:\Program Files (x86)\GNU\GnuPG\pinentry-gtk-2.exe + // C:\Program Files (x86)\GNU\GnuPG\pinentry-qt4.exe + // C:\Program Files (x86)\GNU\GnuPG\pinentry-w32.exe + // C:\Program Files (x86)\GNU\GnuPG\pinentry.exe + + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Wow6432Node\GNU\GnuPG`, registry.QUERY_VALUE) + if err != nil { + k, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\GNU\GnuPG`, registry.QUERY_VALUE) + } + if err != nil { + log.Debug("- FindPinentry: can't open registry") + } + defer k.Close() + + installDir, _, err := k.GetStringValue("Install Directory") + if err != nil { + log.Debug("- FindPinentry: can't get string from registry") + } + + extraPaths := []string{} + + log.Debug("+ FindPinentry()") + + cmds := []string{ + "pinentry-gtk-2.exe", + "pinentry-qt4.exe", + "pinentry-w32.exe", + "pinentry.exe", + } + + // First, look where the registry points + for _, c := range cmds { + full := filepath.Join(installDir, c) + log.Debug("| (registry) Looking for %s", full) + _, err := exec.LookPath(full) + if err == nil { + return full, nil + } + } + + // Look in program files, just in case + extraPaths = append(extraPaths, os.Getenv("ProgramFiles")) + extraPaths = append(extraPaths, os.Getenv("ProgramFiles(x86)")) + + for _, ep := range extraPaths { + for _, c := range cmds { + full := filepath.Join(ep, "GNU", "GnuPG", c) + log.Debug("| Looking for %s", full) + _, err := exec.LookPath(full) + if err == nil { + return full, nil + } + } + } + + for _, ep := range extraPaths { + for _, c := range cmds { + full := filepath.Join(ep, "Gpg4win", "bin", c) + log.Debug("| Looking for %s", full) + _, err := exec.LookPath(full) + if err == nil { + return full, nil + } + } + } + + log.Debug("- FindPinentry: none found") + return "", fmt.Errorf("No pinentry found, checked a bunch of different places") +} + +func (pe *Pinentry) GetTerminalName() { + pe.tty = "windows" +} diff --git a/agent/systemauth/pinentry/keybase-pinentry/pinentry_x11.go b/agent/systemauth/pinentry/keybase-pinentry/pinentry_x11.go new file mode 100644 index 0000000..29ca28a --- /dev/null +++ b/agent/systemauth/pinentry/keybase-pinentry/pinentry_x11.go @@ -0,0 +1,14 @@ +// Copyright 2015 Keybase, Inc. All rights reserved. Use of +// this source code is governed by the included BSD license. + +//go:build dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris +// +build dragonfly freebsd linux nacl netbsd openbsd solaris + +package pinentry + +import "os" + +func HasWindows() bool { + //If there is a DISPLAY then we can spawn a window to it. + return len(os.Getenv("DISPLAY")) > 0 +} diff --git a/go.mod b/go.mod index 54094f8..f332f37 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5 github.com/keybase/client/go v0.0.0-20240202160538-668db6be75e4 + github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mitchellh/go-ps v1.0.0 @@ -20,6 +21,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 + golang.org/x/sys v0.15.0 ) require ( @@ -32,7 +34,6 @@ require ( github.com/keybase/go-logging v0.0.0-20231213204715-4b3ff33ba5b6 // indirect github.com/keybase/msgpackzip v0.0.0-20221220225959-4abf538d2b9c // indirect golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 0979382..dfecf91 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/keybase/go-framed-msgpack-rpc v0.0.0-20230103225103-1f052922b096 h1:r github.com/keybase/go-framed-msgpack-rpc v0.0.0-20230103225103-1f052922b096/go.mod h1:XO67nMjltHJ8OsBWnFiDU1F67wR+rtJB21NXtb1TKyA= github.com/keybase/go-jsonw v0.0.0-20200325173637-df90f282c233 h1:zLk+cB/0ShMCBcgBOXYgellLZiZahXFicJleKyrlqiM= github.com/keybase/go-jsonw v0.0.0-20200325173637-df90f282c233/go.mod h1:lofKQwj13L0/7ji5VYaY0257JDlQE2BRRf+rI2Vk1rU= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/keybase/go-logging v0.0.0-20231213204715-4b3ff33ba5b6 h1:H4IvZdHXpeK963LgCMbTcEviEal4891UGf2iOqOGL94= github.com/keybase/go-logging v0.0.0-20231213204715-4b3ff33ba5b6/go.mod h1:0yOEB+QF1Ega1Cr7oMKb3yUAc3C9/eg6fBHB5HLP7AA= github.com/keybase/msgpackzip v0.0.0-20221220225959-4abf538d2b9c h1:PRG2AXSelSy7MiDI+PwJR2QSqI1N3OybRUutsMiHtpo=