github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/libkb/util.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package libkb
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"crypto/rand"
    10  	"crypto/sha256"
    11  	"encoding/base32"
    12  	"encoding/base64"
    13  	"encoding/hex"
    14  	"errors"
    15  	"fmt"
    16  	"io"
    17  	"math"
    18  	"math/big"
    19  	"net/url"
    20  	"os"
    21  	"os/exec"
    22  	"os/user"
    23  	"path/filepath"
    24  	"regexp"
    25  	"runtime"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"syscall"
    30  	"time"
    31  	"unicode"
    32  	"unicode/utf8"
    33  
    34  	"github.com/keybase/client/go/kbcrypto"
    35  	"github.com/keybase/client/go/kbun"
    36  	"github.com/keybase/client/go/logger"
    37  	"github.com/keybase/client/go/profiling"
    38  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    39  	"github.com/keybase/clockwork"
    40  	"github.com/keybase/go-codec/codec"
    41  	jsonw "github.com/keybase/go-jsonw"
    42  	"golang.org/x/net/context"
    43  )
    44  
    45  // PrereleaseBuild can be set at compile time for prerelease builds.
    46  // CAUTION: Don't change the name of this variable without grepping for
    47  // occurrences in shell scripts!
    48  var PrereleaseBuild string
    49  
    50  // VersionString returns semantic version string
    51  func VersionString() string {
    52  	if PrereleaseBuild != "" {
    53  		return fmt.Sprintf("%s-%s", Version, PrereleaseBuild)
    54  	}
    55  	return Version
    56  }
    57  
    58  func ErrToOkPtr(err *error) string {
    59  	if err == nil {
    60  		return "ok"
    61  	}
    62  	return ErrToOk(*err)
    63  }
    64  
    65  func ErrToOk(err error) string {
    66  	if err == nil {
    67  		return "ok"
    68  	}
    69  	return fmt.Sprintf("ERROR: %v", err)
    70  }
    71  
    72  // exists returns whether the given file or directory exists or not
    73  func FileExists(path string) (bool, error) {
    74  	_, err := os.Stat(path)
    75  	if err == nil {
    76  		return true, nil
    77  	}
    78  	if os.IsNotExist(err) {
    79  		return false, nil
    80  	}
    81  	return false, err
    82  }
    83  
    84  func MakeParentDirs(log SkinnyLogger, filename string) error {
    85  	dir := filepath.Dir(filename)
    86  	exists, err := FileExists(dir)
    87  	if err != nil {
    88  		log.Errorf("Can't see if parent dir %s exists", dir)
    89  		return err
    90  	}
    91  
    92  	if !exists {
    93  		err = os.MkdirAll(dir, PermDir)
    94  		if err != nil {
    95  			log.Errorf("Can't make parent dir %s", dir)
    96  			return err
    97  		}
    98  		log.Debug("Created parent directory %s", dir)
    99  	}
   100  	return nil
   101  }
   102  
   103  func FastByteArrayEq(a, b []byte) bool {
   104  	return kbcrypto.FastByteArrayEq(a, b)
   105  }
   106  
   107  func SecureByteArrayEq(a, b []byte) bool {
   108  	return kbcrypto.SecureByteArrayEq(a, b)
   109  }
   110  
   111  func FormatTime(tm time.Time) string {
   112  	layout := "2006-01-02 15:04:05 MST"
   113  	return tm.Format(layout)
   114  }
   115  
   116  func Cicmp(s1, s2 string) bool {
   117  	return strings.EqualFold(s1, s2)
   118  }
   119  
   120  func TrimCicmp(s1, s2 string) bool {
   121  	return Cicmp(strings.TrimSpace(s1), strings.TrimSpace(s2))
   122  }
   123  
   124  func NameTrim(s string) string {
   125  	s = strings.ToLower(strings.TrimSpace(s))
   126  	strip := func(r rune) rune {
   127  		switch {
   128  		case r == '_', r == '-', r == '+', r == '\'':
   129  			return -1
   130  		case unicode.IsSpace(r):
   131  			return -1
   132  		}
   133  		return r
   134  
   135  	}
   136  	return strings.Map(strip, s)
   137  }
   138  
   139  // NameCmp removes whitespace and underscores, compares tolower.
   140  func NameCmp(n1, n2 string) bool {
   141  	return NameTrim(n1) == NameTrim(n2)
   142  }
   143  
   144  func IsLowercase(s string) bool {
   145  	return strings.ToLower(s) == s
   146  }
   147  
   148  func PickFirstError(errors ...error) error {
   149  	for _, e := range errors {
   150  		if e != nil {
   151  			return e
   152  		}
   153  	}
   154  	return nil
   155  }
   156  
   157  type FirstErrorPicker struct {
   158  	e     error
   159  	count int
   160  }
   161  
   162  func (p *FirstErrorPicker) Push(e error) {
   163  	if e != nil {
   164  		p.count++
   165  		if p.e == nil {
   166  			p.e = e
   167  		}
   168  	}
   169  }
   170  
   171  func (p *FirstErrorPicker) Count() int {
   172  	return p.count
   173  }
   174  
   175  func (p *FirstErrorPicker) Error() error {
   176  	return p.e
   177  }
   178  
   179  func GiveMeAnS(i int) string {
   180  	if i != 1 {
   181  		return "s"
   182  	}
   183  	return ""
   184  }
   185  
   186  func KeybaseEmailAddress(s string) string {
   187  	return s + "@keybase.io"
   188  }
   189  
   190  func DrainPipe(rc io.Reader, sink func(string)) error {
   191  	scanner := bufio.NewScanner(rc)
   192  	for scanner.Scan() {
   193  		sink(scanner.Text())
   194  	}
   195  	return scanner.Err()
   196  }
   197  
   198  type SafeWriter interface {
   199  	GetFilename() string
   200  	WriteTo(io.Writer) (int64, error)
   201  }
   202  
   203  type SafeWriteLogger interface {
   204  	Debug(format string, args ...interface{})
   205  	Errorf(format string, args ...interface{})
   206  }
   207  
   208  // SafeWriteToFile to safely write to a file. Use mode=0 for default permissions.
   209  func safeWriteToFileOnce(g SafeWriteLogger, t SafeWriter, mode os.FileMode) (err error) {
   210  	fn := t.GetFilename()
   211  	g.Debug("+ SafeWriteToFile(%q)", fn)
   212  	defer func() {
   213  		g.Debug("- SafeWriteToFile(%q) -> %s", fn, ErrToOk(err))
   214  	}()
   215  
   216  	tmpfn, tmp, err := OpenTempFile(fn, "", mode)
   217  	if err != nil {
   218  		return err
   219  	}
   220  	g.Debug("| Temporary file generated: %s", tmpfn)
   221  	defer tmp.Close()
   222  	defer func() { _ = ShredFile(tmpfn) }()
   223  
   224  	g.Debug("| WriteTo %s", tmpfn)
   225  	n, err := t.WriteTo(tmp)
   226  	if err != nil {
   227  		g.Errorf("| Error writing temporary file %s: %s", tmpfn, err)
   228  		return err
   229  	}
   230  	if n != 0 {
   231  		// unfortunately, some implementations always return 0 for the number
   232  		// of bytes written, so not much info there, but will log it when
   233  		// it isn't 0.
   234  		g.Debug("| bytes written to temporary file %s: %d", tmpfn, n)
   235  	}
   236  
   237  	if err := tmp.Sync(); err != nil {
   238  		g.Errorf("| Error syncing temporary file %s: %s", tmpfn, err)
   239  		return err
   240  	}
   241  
   242  	if err := tmp.Close(); err != nil {
   243  		g.Errorf("| Error closing temporary file %s: %s", tmpfn, err)
   244  		return err
   245  	}
   246  
   247  	g.Debug("| Renaming temporary file %s -> permanent file %s", tmpfn, fn)
   248  	if err := os.Rename(tmpfn, fn); err != nil {
   249  		g.Errorf("| Error renaming temporary file %s -> permanent file %s: %s", tmpfn, fn, err)
   250  		return err
   251  	}
   252  
   253  	if runtime.GOOS == "android" {
   254  		g.Debug("| Android extra checks in safeWriteToFile")
   255  		info, err := os.Stat(fn)
   256  		if err != nil {
   257  			g.Errorf("| Error os.Stat(%s): %s", fn, err)
   258  			return err
   259  		}
   260  		g.Debug("| File info: name = %s", info.Name())
   261  		g.Debug("| File info: size = %d", info.Size())
   262  		g.Debug("| File info: mode = %s", info.Mode())
   263  		g.Debug("| File info: mod time = %s", info.ModTime())
   264  
   265  		g.Debug("| Android extra checks done")
   266  	}
   267  
   268  	g.Debug("| Done writing to file %s", fn)
   269  
   270  	return nil
   271  }
   272  
   273  // Pluralize returns pluralized string with value.
   274  // For example,
   275  //
   276  //	Pluralize(1, "zebra", "zebras", true) => "1 zebra"
   277  //	Pluralize(2, "zebra", "zebras", true) => "2 zebras"
   278  //	Pluralize(2, "zebra", "zebras", false) => "zebras"
   279  func Pluralize(n int, singular string, plural string, nshow bool) string {
   280  	if n == 1 {
   281  		if nshow {
   282  			return fmt.Sprintf("%d %s", n, singular)
   283  		}
   284  		return singular
   285  	}
   286  	if nshow {
   287  		return fmt.Sprintf("%d %s", n, plural)
   288  	}
   289  	return plural
   290  }
   291  
   292  // Contains returns true if string is contained in string slice
   293  func Contains(s string, list []string) bool {
   294  	return IsIn(s, list, false)
   295  }
   296  
   297  // IsIn checks for needle in haystack, ci means case-insensitive.
   298  func IsIn(needle string, haystack []string, ci bool) bool {
   299  	for _, h := range haystack {
   300  		if (ci && Cicmp(h, needle)) || (!ci && h == needle) {
   301  			return true
   302  		}
   303  	}
   304  	return false
   305  }
   306  
   307  // Found regex here: http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
   308  var hostnameRE = regexp.MustCompile("^(?i:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$")
   309  
   310  func IsValidHostname(s string) bool {
   311  	parts := strings.Split(s, ".")
   312  	// Found regex here: http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
   313  	if len(parts) < 2 {
   314  		return false
   315  	}
   316  	for _, p := range parts {
   317  		if !hostnameRE.MatchString(p) {
   318  			return false
   319  		}
   320  	}
   321  	// TLDs must be >=2 chars
   322  	return len(parts[len(parts)-1]) >= 2
   323  }
   324  
   325  var phoneAssertionRE = regexp.MustCompile(`^[1-9]\d{1,14}$`)
   326  
   327  // IsPossiblePhoneNumberAssertion checks if s is string of digits without a `+`
   328  // prefix for SBS assertions
   329  func IsPossiblePhoneNumberAssertion(s string) bool {
   330  	return phoneAssertionRE.MatchString(s)
   331  }
   332  
   333  var phoneRE = regexp.MustCompile(`^\+[1-9]\d{1,14}$`)
   334  
   335  // IsPossiblePhoneNumber checks if s is string of digits in phone number format
   336  func IsPossiblePhoneNumber(phone keybase1.PhoneNumber) error {
   337  	if !phoneRE.MatchString(string(phone)) {
   338  		return fmt.Errorf("Invalid phone number, expected +11234567890 format")
   339  	}
   340  	return nil
   341  }
   342  
   343  type Random interface {
   344  	// RndRange returns a uniformly random integer between low and high inclusive
   345  	RndRange(low, high int64) (res int64, err error)
   346  }
   347  
   348  // SecureRandom internally uses the cryptographically secure crypto/rand as a
   349  // source of randomness.
   350  type SecureRandom struct{}
   351  
   352  func (r *SecureRandom) RndRange(low, high int64) (res int64, err error) {
   353  	if low > high {
   354  		return 0, fmt.Errorf("SecureRandom error: [%v,%v] is not a valid range", low, high)
   355  	}
   356  	rangeBig := big.NewInt(high - low + 1)
   357  	big, err := rand.Int(rand.Reader, rangeBig)
   358  	if err != nil {
   359  		return 0, err
   360  	}
   361  	return low + big.Int64(), nil
   362  }
   363  
   364  var _ Random = (*SecureRandom)(nil)
   365  
   366  func RandBytes(length int) ([]byte, error) {
   367  	var n int
   368  	var err error
   369  	buf := make([]byte, length)
   370  	if n, err = rand.Read(buf); err != nil {
   371  		return nil, err
   372  	}
   373  	// rand.Read uses io.ReadFull internally, so this check should never fail.
   374  	if n != length {
   375  		return nil, fmt.Errorf("RandBytes got too few bytes, %d < %d", n, length)
   376  	}
   377  	return buf, nil
   378  }
   379  
   380  func RandBytesWithSuffix(length int, suffix byte) ([]byte, error) {
   381  	buf, err := RandBytes(length)
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  	buf[len(buf)-1] = suffix
   386  	return buf, nil
   387  }
   388  
   389  func XORBytes(dst, a, b []byte) int {
   390  	n := len(a)
   391  	if len(b) < n {
   392  		n = len(b)
   393  	}
   394  	for i := 0; i < n; i++ {
   395  		dst[i] = a[i] ^ b[i]
   396  	}
   397  	return n
   398  }
   399  
   400  // The standard time.Unix() converter interprets 0 as the Unix epoch (1970).
   401  // But in PGP, an expiry time of zero indicates that a key never expires, and
   402  // it would be nice to be able to check for that case with Time.IsZero(). This
   403  // conversion special-cases 0 to be time.Time's zero-value (1 AD), so that we
   404  // get that nice property.
   405  func UnixToTimeMappingZero(unixTime int64) time.Time {
   406  	if unixTime == 0 {
   407  		var zeroTime time.Time
   408  		return zeroTime
   409  	}
   410  	return time.Unix(unixTime, 0)
   411  }
   412  
   413  func Unquote(data []byte) string { return keybase1.Unquote(data) }
   414  
   415  func HexDecodeQuoted(data []byte) ([]byte, error) {
   416  	return hex.DecodeString(Unquote(data))
   417  }
   418  
   419  func IsArmored(buf []byte) bool {
   420  	return bytes.HasPrefix(bytes.TrimSpace(buf), []byte("-----"))
   421  }
   422  
   423  func RandInt64() (int64, error) {
   424  	max := big.NewInt(math.MaxInt64)
   425  	x, err := rand.Int(rand.Reader, max)
   426  	if err != nil {
   427  		return 0, err
   428  	}
   429  	return x.Int64(), nil
   430  }
   431  
   432  func RandInt() (int, error) {
   433  	x, err := RandInt64()
   434  	if err != nil {
   435  		return 0, err
   436  	}
   437  	return int(x), nil
   438  }
   439  
   440  func RandIntn(n int) int {
   441  	x, err := RandInt()
   442  	if err != nil {
   443  		panic(fmt.Sprintf("RandInt error: %s", err))
   444  	}
   445  	return x % n
   446  }
   447  
   448  // MakeURI makes a URI string out of the given protocol and
   449  // host strings, adding necessary punctuation in between.
   450  func MakeURI(prot string, host string) string {
   451  	if prot == "" {
   452  		return host
   453  	}
   454  	if prot[len(prot)-1] != ':' {
   455  		prot += ":"
   456  	}
   457  	return prot + "//" + host
   458  }
   459  
   460  // RemoveNilErrors returns error slice with ni errors removed.
   461  func RemoveNilErrors(errs []error) []error {
   462  	var r []error
   463  	for _, err := range errs {
   464  		if err != nil {
   465  			r = append(r, err)
   466  		}
   467  	}
   468  	return r
   469  }
   470  
   471  // CombineErrors returns a single error for multiple errors, or nil if none.
   472  func CombineErrors(errs ...error) error {
   473  	errs = RemoveNilErrors(errs)
   474  	if len(errs) == 0 {
   475  		return nil
   476  	} else if len(errs) == 1 {
   477  		return errs[0]
   478  	}
   479  
   480  	msgs := []string{}
   481  	for _, err := range errs {
   482  		msgs = append(msgs, err.Error())
   483  	}
   484  	return fmt.Errorf("There were multiple errors: %s", strings.Join(msgs, "; "))
   485  }
   486  
   487  // IsDirEmpty returns whether directory has any files.
   488  func IsDirEmpty(dir string) (bool, error) {
   489  	f, err := os.Open(dir)
   490  	if err != nil {
   491  		return false, err
   492  	}
   493  	defer f.Close()
   494  
   495  	_, err = f.Readdir(1)
   496  	if err == io.EOF {
   497  		return true, nil
   498  	}
   499  	return false, err // Either not empty or error, suits both cases
   500  }
   501  
   502  // RandString returns random (base32) string with prefix.
   503  func RandString(prefix string, numbytes int) (string, error) {
   504  	buf, err := RandBytes(numbytes)
   505  	if err != nil {
   506  		return "", err
   507  	}
   508  	str := base32.StdEncoding.EncodeToString(buf)
   509  	if prefix != "" {
   510  		str = strings.Join([]string{prefix, str}, "")
   511  	}
   512  	return str, nil
   513  }
   514  
   515  func RandStringB64(numTriads int) string {
   516  	buf, err := RandBytes(numTriads * 3)
   517  	if err != nil {
   518  		return ""
   519  	}
   520  	return base64.URLEncoding.EncodeToString(buf)
   521  }
   522  
   523  func RandHexString(prefix string, numbytes int) (string, error) {
   524  	buf, err := RandBytes(numbytes)
   525  	if err != nil {
   526  		return "", err
   527  	}
   528  	str := hex.EncodeToString(buf)
   529  	return prefix + str, nil
   530  }
   531  
   532  func Trace(log logger.Logger, msg string, err *error) func() {
   533  	log = log.CloneWithAddedDepth(1)
   534  	log.Debug("+ %s", msg)
   535  	start := time.Now()
   536  	return func() { log.Debug("- %s -> %s [time=%s]", msg, ErrToOkPtr(err), time.Since(start)) }
   537  }
   538  
   539  func CTrace(ctx context.Context, log logger.Logger, msg string, err *error, cl clockwork.Clock) func() {
   540  	log = log.CloneWithAddedDepth(1)
   541  	log.CDebugf(ctx, "+ %s", msg)
   542  	start := cl.Now()
   543  	return func() {
   544  		if err != nil && *err != nil {
   545  			log.CDebugf(ctx, "- %s -> %v %T [time=%s]", msg, *err, err, cl.Since(start))
   546  		} else {
   547  			log.CDebugf(ctx, "- %s -> ok [time=%s]", msg, cl.Since(start))
   548  		}
   549  	}
   550  }
   551  
   552  func (g *GlobalContext) Trace(msg string, err *error) func() {
   553  	return Trace(g.Log.CloneWithAddedDepth(1), msg, err)
   554  }
   555  func (g *GlobalContext) CTrace(ctx context.Context, msg string, err *error) func() {
   556  	return CTrace(ctx, g.Log.CloneWithAddedDepth(1), msg, err, g.Clock())
   557  }
   558  func (g *GlobalContext) CPerfTrace(ctx context.Context, msg string, err *error) func() {
   559  	return CTrace(ctx, g.PerfLog, msg, err, g.Clock())
   560  }
   561  
   562  func (g *GlobalContext) CVTrace(ctx context.Context, lev VDebugLevel, msg string, err *error) func() {
   563  	cl := g.Clock()
   564  	g.VDL.CLogf(ctx, lev, "+ %s", msg)
   565  	start := cl.Now()
   566  	return func() {
   567  		g.VDL.CLogf(ctx, lev, "- %s -> %v [time=%s]", msg, ErrToOkPtr(err), cl.Since(start))
   568  	}
   569  }
   570  
   571  func (g *GlobalContext) CTimeTracer(ctx context.Context, label string, enabled bool) profiling.TimeTracer {
   572  	if enabled {
   573  		return profiling.NewTimeTracer(ctx, g.Log.CloneWithAddedDepth(1), g.Clock(), label)
   574  	}
   575  	return profiling.NewSilentTimeTracer()
   576  }
   577  
   578  func (g *GlobalContext) CTimeBuckets(ctx context.Context) (context.Context, *profiling.TimeBuckets) {
   579  	return profiling.WithTimeBuckets(ctx, g.Clock(), g.Log)
   580  }
   581  
   582  // SplitByRunes splits string by runes
   583  func SplitByRunes(s string, separators []rune) []string {
   584  	f := func(r rune) bool {
   585  		for _, s := range separators {
   586  			if r == s {
   587  				return true
   588  			}
   589  		}
   590  		return false
   591  	}
   592  	return strings.FieldsFunc(s, f)
   593  }
   594  
   595  // SplitPath return string split by path separator: SplitPath("/a/b/c") => []string{"a", "b", "c"}
   596  func SplitPath(s string) []string {
   597  	return SplitByRunes(s, []rune{filepath.Separator})
   598  }
   599  
   600  // IsSystemAdminUser returns true if current user is root or admin (system user, not Keybase user).
   601  // WARNING: You shouldn't rely on this for security purposes.
   602  func IsSystemAdminUser() (isAdminUser bool, match string, err error) {
   603  	u, err := user.Current()
   604  	if err != nil {
   605  		return
   606  	}
   607  
   608  	if u.Uid == "0" {
   609  		match = "Uid: 0"
   610  		isAdminUser = true
   611  		return
   612  	}
   613  	return
   614  }
   615  
   616  // DigestForFileAtPath returns a SHA256 digest for file at specified path
   617  func DigestForFileAtPath(path string) (string, error) {
   618  	f, err := os.Open(path)
   619  	if err != nil {
   620  		return "", err
   621  	}
   622  	defer f.Close()
   623  	return Digest(f)
   624  }
   625  
   626  // Digest returns a SHA256 digest
   627  func Digest(r io.Reader) (string, error) {
   628  	hasher := sha256.New()
   629  	if _, err := io.Copy(hasher, r); err != nil {
   630  		return "", err
   631  	}
   632  	digest := hex.EncodeToString(hasher.Sum(nil))
   633  	return digest, nil
   634  }
   635  
   636  // TimeLog calls out with the time since start.  Use like this:
   637  //
   638  //	defer TimeLog("MyFunc", time.Now(), e.G().Log.Warning)
   639  func TimeLog(name string, start time.Time, out func(string, ...interface{})) {
   640  	out("time> %s: %s", name, time.Since(start))
   641  }
   642  
   643  // CTimeLog calls out with the time since start.  Use like this:
   644  //
   645  //	defer CTimeLog(ctx, "MyFunc", time.Now(), e.G().Log.Warning)
   646  func CTimeLog(ctx context.Context, name string, start time.Time, out func(context.Context, string, ...interface{})) {
   647  	out(ctx, "time> %s: %s", name, time.Since(start))
   648  }
   649  
   650  var wsRE = regexp.MustCompile(`\s+`)
   651  
   652  func WhitespaceNormalize(s string) string {
   653  	v := wsRE.Split(s, -1)
   654  	if len(v) > 0 && len(v[0]) == 0 {
   655  		v = v[1:]
   656  	}
   657  	if len(v) > 0 && len(v[len(v)-1]) == 0 {
   658  		v = v[0 : len(v)-1]
   659  	}
   660  	return strings.Join(v, " ")
   661  }
   662  
   663  // JoinPredicate joins strings with predicate
   664  func JoinPredicate(arr []string, delimeter string, f func(s string) bool) string {
   665  	arrNew := make([]string, 0, len(arr))
   666  	for _, s := range arr {
   667  		if f(s) {
   668  			arrNew = append(arrNew, s)
   669  		}
   670  	}
   671  	return strings.Join(arrNew, delimeter)
   672  }
   673  
   674  // LogTagsFromContext is a wrapper around logger.LogTagsFromContext
   675  // that simply casts the result to the type expected by
   676  // rpc.Connection.
   677  func LogTagsFromContext(ctx context.Context) (map[interface{}]string, bool) {
   678  	tags, ok := logger.LogTagsFromContext(ctx)
   679  	return map[interface{}]string(tags), ok
   680  }
   681  
   682  func MakeByte24(a []byte) [24]byte {
   683  	const n = 24
   684  	if len(a) != n {
   685  		panic(fmt.Sprintf("MakeByte expected len %v but got %v slice", n, len(a)))
   686  	}
   687  	var b [n]byte
   688  	copy(b[:], a)
   689  	return b
   690  }
   691  
   692  func MakeByte32(a []byte) [32]byte {
   693  	const n = 32
   694  	if len(a) != n {
   695  		panic(fmt.Sprintf("MakeByte expected len %v but got %v slice", n, len(a)))
   696  	}
   697  	var b [n]byte
   698  	copy(b[:], a)
   699  	return b
   700  }
   701  
   702  func MakeByte32Soft(a []byte) ([32]byte, error) {
   703  	const n = 32
   704  	var b [n]byte
   705  	if len(a) != n {
   706  		return b, fmt.Errorf("MakeByte expected len %v but got %v slice", n, len(a))
   707  	}
   708  	copy(b[:], a)
   709  	return b, nil
   710  }
   711  
   712  // Sleep until `deadline` or until `ctx` is canceled, whichever occurs first.
   713  // Returns an error BUT the error is not really an error.
   714  // It is nil if the sleep finished, and the non-nil result of Context.Err()
   715  func SleepUntilWithContext(ctx context.Context, clock clockwork.Clock, deadline time.Time) error {
   716  	if ctx == nil {
   717  		// should not happen
   718  		clock.AfterTime(deadline)
   719  		return nil
   720  	}
   721  	select {
   722  	case <-clock.AfterTime(deadline):
   723  		return nil
   724  	case <-ctx.Done():
   725  		return ctx.Err()
   726  	}
   727  }
   728  
   729  func Sleep(ctx context.Context, duration time.Duration) error {
   730  	timer := time.NewTimer(duration)
   731  	select {
   732  	case <-ctx.Done():
   733  		timer.Stop()
   734  		return ctx.Err()
   735  	case <-timer.C:
   736  		return nil
   737  	}
   738  }
   739  
   740  func UseCITime(g *GlobalContext) bool {
   741  	return g.GetEnv().RunningInCI() || g.GetEnv().GetSlowGregorConn()
   742  }
   743  
   744  func CITimeMultiplier(g *GlobalContext) time.Duration {
   745  	if UseCITime(g) {
   746  		return time.Duration(3)
   747  	}
   748  	return time.Duration(1)
   749  }
   750  
   751  func CanExec(p string) error {
   752  	return canExec(p)
   753  }
   754  
   755  func CurrentBinaryRealpath() (string, error) {
   756  	if IsMobilePlatform() {
   757  		return "mobile-binary-location-unknown", nil
   758  	}
   759  
   760  	executable, err := os.Executable()
   761  	if err != nil {
   762  		return "", err
   763  	}
   764  	return filepath.EvalSymlinks(executable)
   765  }
   766  
   767  var adminFeatureList = map[keybase1.UID]bool{
   768  	"23260c2ce19420f97b58d7d95b68ca00": true, // | chris        |
   769  	"dbb165b7879fe7b1174df73bed0b9500": true, // | max          |
   770  	"1563ec26dc20fd162a4f783551141200": true, // | patrick      |
   771  	"d73af57c418a917ba6665575eba13500": true, // | adamjspooner |
   772  	"95e88f2087e480cae28f08d81554bc00": true, // | mikem        |
   773  	"d1b3a5fa977ce53da2c2142a4511bc00": true, // | joshblum     |
   774  	"08abe80bd2da8984534b2d8f7b12c700": true, // | songgao      |
   775  	"237e85db5d939fbd4b84999331638200": true, // | cjb          |
   776  	"46fa8104092d0a680ad854bfc8507700": true, // | xgess        |
   777  	"69da56f622a2ac750b8e590c3658a700": true, // | jzila        |
   778  	"ef2e49961eddaa77094b45ed635cfc00": true, // | strib        |
   779  	"9403ede05906b942fd7361f40a679500": true, // | jinyang      |
   780  	"e0b4166c9c839275cf5633ff65c3e819": true, // | chrisnojima  |
   781  	"5f72055750c37c02a630122781508219": true, // | jakob223     |
   782  	"d95f137b3b4a3600bc9e39350adba819": true, // | cecileb      |
   783  	"eb08cb06e608ea41bd893946445d7919": true, // | mlsteele     |
   784  	"4a2c5d27346497ad64e3b7d457a1f919": true, // | pzduniak     |
   785  	"743338e8d5987e0e5077f0fddc763f19": true, // | taruti       |
   786  	"ee71dbc8e4e3e671e29a94caef5e1b19": true, // | zapu         |
   787  	"8c7c57995cd14780e351fc90ca7dc819": true, // | ayoubd       |
   788  	"b848bce3d54a76e4da323aad2957e819": true, // | modalduality |
   789  }
   790  
   791  // IsKeybaseAdmin returns true if uid is a keybase admin.
   792  func IsKeybaseAdmin(uid keybase1.UID) bool {
   793  	return adminFeatureList[uid]
   794  }
   795  
   796  // MobilePermissionDeniedCheck panics if err is a permission denied error
   797  // and if app is a mobile app. This has caused issues opening config.json
   798  // and secretkeys files, where it seems to be stuck in a permission
   799  // denied state and force-killing the app is the only option.
   800  func MobilePermissionDeniedCheck(g *GlobalContext, err error, msg string) {
   801  	if !os.IsPermission(err) {
   802  		return
   803  	}
   804  	if g.GetAppType() != MobileAppType {
   805  		return
   806  	}
   807  	g.Log.Warning("file open permission denied on mobile (%s): %s", msg, err)
   808  	os.Exit(4)
   809  }
   810  
   811  // IsNoSpaceOnDeviceError will return true if err is an `os` error
   812  // for "no space left on device".
   813  func IsNoSpaceOnDeviceError(err error) bool {
   814  	if err == nil {
   815  		return false
   816  	}
   817  	switch err := err.(type) {
   818  	case NoSpaceOnDeviceError:
   819  		return true
   820  	case *os.PathError:
   821  		return err.Err == syscall.ENOSPC
   822  	case *os.LinkError:
   823  		return err.Err == syscall.ENOSPC
   824  	case *os.SyscallError:
   825  		return err.Err == syscall.ENOSPC
   826  	}
   827  
   828  	return false
   829  }
   830  
   831  func ShredFile(filename string) error {
   832  	stat, err := os.Stat(filename)
   833  	if err != nil {
   834  		return err
   835  	}
   836  	if stat.IsDir() {
   837  		return errors.New("cannot shred a directory")
   838  	}
   839  	size := int(stat.Size())
   840  
   841  	defer os.Remove(filename)
   842  
   843  	for i := 0; i < 3; i++ {
   844  		noise, err := RandBytes(size)
   845  		if err != nil {
   846  			return err
   847  		}
   848  		if err := os.WriteFile(filename, noise, stat.Mode().Perm()); err != nil {
   849  			return err
   850  		}
   851  	}
   852  
   853  	return os.Remove(filename)
   854  }
   855  
   856  func MPackEncode(input interface{}) ([]byte, error) {
   857  	mh := codec.MsgpackHandle{WriteExt: true}
   858  	var data []byte
   859  	enc := codec.NewEncoderBytes(&data, &mh)
   860  	if err := enc.Encode(input); err != nil {
   861  		return nil, err
   862  	}
   863  	return data, nil
   864  }
   865  
   866  func MPackDecode(data []byte, res interface{}) error {
   867  	mh := codec.MsgpackHandle{WriteExt: true}
   868  	dec := codec.NewDecoderBytes(data, &mh)
   869  	err := dec.Decode(res)
   870  	return err
   871  }
   872  
   873  type NoiseBytes [noiseFileLen]byte
   874  
   875  func MakeNoise() (nb NoiseBytes, err error) {
   876  	noise, err := RandBytes(noiseFileLen)
   877  	if err != nil {
   878  		return nb, err
   879  	}
   880  	copy(nb[:], noise)
   881  	return nb, nil
   882  }
   883  
   884  func NoiseXOR(secret [32]byte, noise NoiseBytes) ([]byte, error) {
   885  	sum := sha256.Sum256(noise[:])
   886  	if len(sum) != len(secret) {
   887  		return nil, errors.New("secret or sha256.Size is no longer 32")
   888  	}
   889  
   890  	xor := make([]byte, len(sum))
   891  	for i := 0; i < len(sum); i++ {
   892  		xor[i] = sum[i] ^ secret[i]
   893  	}
   894  
   895  	return xor, nil
   896  }
   897  
   898  // ForceWallClock takes a multi-personality Go time and converts it to
   899  // a regular old WallClock time.
   900  func ForceWallClock(t time.Time) time.Time {
   901  	return t.Round(0)
   902  }
   903  
   904  // Decode decodes src into dst.
   905  // Errors unless all of:
   906  // - src is valid hex
   907  // - src decodes into exactly len(dst) bytes
   908  func DecodeHexFixed(dst, src []byte) error {
   909  	// hex.Decode is wrapped because it does not error on short reads and panics on long reads.
   910  	if len(src)%2 == 1 {
   911  		return hex.ErrLength
   912  	}
   913  	if len(dst) != hex.DecodedLen(len(src)) {
   914  		return NewHexWrongLengthError(fmt.Sprintf(
   915  			"error decoding fixed-length hex: expected %v bytes but got %v", len(dst), hex.DecodedLen(len(src))))
   916  	}
   917  	n, err := hex.Decode(dst, src)
   918  	if err != nil {
   919  		return err
   920  	}
   921  	if n != len(dst) {
   922  		return NewHexWrongLengthError(fmt.Sprintf(
   923  			"error decoding fixed-length hex: expected %v bytes but got %v", len(dst), n))
   924  	}
   925  	return nil
   926  }
   927  
   928  func IsIOS() bool {
   929  	return isIOS
   930  }
   931  
   932  // AcquireWithContext attempts to acquire a lock with a context.
   933  // Returns nil if the lock was acquired.
   934  // Returns an error if it was not. The error is from ctx.Err().
   935  func AcquireWithContext(ctx context.Context, lock sync.Locker) (err error) {
   936  	if err = ctx.Err(); err != nil {
   937  		return err
   938  	}
   939  	acquiredCh := make(chan struct{})
   940  	shouldReleaseCh := make(chan bool, 1)
   941  	go func() {
   942  		lock.Lock()
   943  		close(acquiredCh)
   944  		shouldRelease := <-shouldReleaseCh
   945  		if shouldRelease {
   946  			lock.Unlock()
   947  		}
   948  	}()
   949  	select {
   950  	case <-acquiredCh:
   951  		err = nil
   952  	case <-ctx.Done():
   953  		err = ctx.Err()
   954  	}
   955  	shouldReleaseCh <- err != nil
   956  	return err
   957  }
   958  
   959  // AcquireWithTimeout attempts to acquire a lock with a timeout.
   960  // Convenience wrapper around AcquireWithContext.
   961  // Returns nil if the lock was acquired.
   962  // Returns context.DeadlineExceeded if it was not.
   963  func AcquireWithTimeout(lock sync.Locker, timeout time.Duration) (err error) {
   964  	ctx2, cancel := context.WithTimeout(context.Background(), timeout)
   965  	defer cancel()
   966  	return AcquireWithContext(ctx2, lock)
   967  }
   968  
   969  // AcquireWithContextAndTimeout attempts to acquire a lock with a context and a timeout.
   970  // Convenience wrapper around AcquireWithContext.
   971  // Returns nil if the lock was acquired.
   972  // Returns context.DeadlineExceeded or the error from ctx.Err() if it was not.
   973  func AcquireWithContextAndTimeout(ctx context.Context, lock sync.Locker, timeout time.Duration) (err error) {
   974  	ctx2, cancel := context.WithTimeout(ctx, timeout)
   975  	defer cancel()
   976  	return AcquireWithContext(ctx2, lock)
   977  }
   978  
   979  func Once(f func()) func() {
   980  	var once sync.Once
   981  	return func() {
   982  		once.Do(f)
   983  	}
   984  }
   985  
   986  func RuntimeGroup() keybase1.RuntimeGroup {
   987  	return runtimeGroup(runtime.GOOS)
   988  }
   989  
   990  func runtimeGroup(osname string) keybase1.RuntimeGroup {
   991  	switch osname {
   992  	case "linux", "dragonfly", "freebsd", "netbsd", "openbsd", "android":
   993  		return keybase1.RuntimeGroup_LINUXLIKE
   994  	case "darwin", "ios":
   995  		return keybase1.RuntimeGroup_DARWINLIKE
   996  	case "windows":
   997  		return keybase1.RuntimeGroup_WINDOWSLIKE
   998  	default:
   999  		return keybase1.RuntimeGroup_UNKNOWN
  1000  	}
  1001  }
  1002  
  1003  // execToString returns the space-trimmed output of a command or an error.
  1004  func execToString(bin string, args []string) (string, error) {
  1005  	result, err := exec.Command(bin, args...).Output()
  1006  	if err != nil {
  1007  		return "", err
  1008  	}
  1009  	if result == nil {
  1010  		return "", fmt.Errorf("Nil result")
  1011  	}
  1012  	return strings.TrimSpace(string(result)), nil
  1013  }
  1014  
  1015  var preferredKBFSMountDirs = func() []string {
  1016  	switch RuntimeGroup() {
  1017  	case keybase1.RuntimeGroup_LINUXLIKE:
  1018  		return []string{"/keybase"}
  1019  	case keybase1.RuntimeGroup_DARWINLIKE:
  1020  		return []string{"/keybase", "/Volumes/Keybase"}
  1021  	default:
  1022  		return []string{}
  1023  	}
  1024  }()
  1025  
  1026  func FindPreferredKBFSMountDirs() (mountDirs []string) {
  1027  	for _, mountDir := range preferredKBFSMountDirs {
  1028  		fi, err := os.Lstat(filepath.Join(mountDir, "private"))
  1029  		if err != nil {
  1030  			continue
  1031  		}
  1032  		if fi.Mode()&os.ModeSymlink != 0 {
  1033  			mountDirs = append(mountDirs, mountDir)
  1034  		}
  1035  	}
  1036  	return mountDirs
  1037  }
  1038  
  1039  var kbfsPathInnerRegExp = func() *regexp.Regexp {
  1040  	// e.g. alice@twitter
  1041  	const regularAssertion = `[-_a-zA-Z0-9.+]+@[a-zA-Z.]+`
  1042  	// e.g. [bob@keybase.io]@email
  1043  	const emailAssertion = `\[[-_+a-zA-Z0-9.]+@[-_a-zA-Z0-9.]+\]@[a-zA-Z.]+`
  1044  	const socialAssertion = `(?:` + regularAssertion + `)|(?:` + emailAssertion + `)`
  1045  	const user = `(?:(?:` + kbun.UsernameRE + `)|(?:` + socialAssertion + `))`
  1046  	const usernames = user + `(?:,` + user + `)*`
  1047  	const teamName = kbun.UsernameRE + `(?:\.` + kbun.UsernameRE + `)*`
  1048  	const tlfType = "/(?:private|public|team)$"
  1049  	const suffix = `(?: \([-_a-zA-Z0-9 #]+\))?`
  1050  	const tlf = "/(?:(?:private|public)/" + usernames + "(?:#" + usernames + ")?|team/" + teamName + ")" + suffix + "(?:/|$)"
  1051  	const specialFiles = "/(?:.kbfs_.+)"
  1052  	return regexp.MustCompile(`^(?:(?:` + tlf + `)|(?:` + tlfType + `)|(?:` + specialFiles + `))`)
  1053  }()
  1054  
  1055  // IsKBFSAfterKeybasePath returns true if afterKeybase, after prefixed by
  1056  // /keybase, is a valid KBFS path.
  1057  func IsKBFSAfterKeybasePath(afterKeybase string) bool {
  1058  	return len(afterKeybase) == 0 || kbfsPathInnerRegExp.MatchString(afterKeybase)
  1059  }
  1060  
  1061  func getKBFSAfterMountPath(afterKeybase string, isWindows bool) string {
  1062  	afterMount := afterKeybase
  1063  	if len(afterMount) == 0 {
  1064  		afterMount = "/"
  1065  	}
  1066  
  1067  	if !isWindows {
  1068  		return afterMount
  1069  	}
  1070  
  1071  	// Encode path names for Windows
  1072  	elems := strings.Split(afterMount, "/")
  1073  	for i, elem := range elems {
  1074  		elems[i] = EncodeKbfsNameForWindows(elem)
  1075  	}
  1076  	return strings.Join(elems, "\\")
  1077  }
  1078  
  1079  func getKBFSDeeplinkPath(afterKeybase string) string {
  1080  	if len(afterKeybase) == 0 {
  1081  		return ""
  1082  	}
  1083  	var segments []string
  1084  	for _, segment := range strings.Split(afterKeybase, "/") {
  1085  		segments = append(segments, url.PathEscape(segment))
  1086  	}
  1087  	return "keybase:/" + strings.Join(segments, "/")
  1088  }
  1089  
  1090  func GetKBFSPathInfo(standardPath string) (pathInfo keybase1.KBFSPathInfo, err error) {
  1091  	const slashKeybase = "/keybase"
  1092  	if !strings.HasPrefix(standardPath, slashKeybase) {
  1093  		return keybase1.KBFSPathInfo{}, errors.New("not a KBFS path")
  1094  	}
  1095  
  1096  	afterKeybase := standardPath[len(slashKeybase):]
  1097  
  1098  	if !IsKBFSAfterKeybasePath(afterKeybase) {
  1099  		return keybase1.KBFSPathInfo{}, errors.New("not a KBFS path")
  1100  	}
  1101  
  1102  	return keybase1.KBFSPathInfo{
  1103  		StandardPath:           standardPath,
  1104  		DeeplinkPath:           getKBFSDeeplinkPath(afterKeybase),
  1105  		PlatformAfterMountPath: getKBFSAfterMountPath(afterKeybase, RuntimeGroup() == keybase1.RuntimeGroup_WINDOWSLIKE),
  1106  	}, nil
  1107  }
  1108  
  1109  func GetSafeFilename(filename string) (safeFilename string) {
  1110  	filename = filepath.Base(filename)
  1111  	if !utf8.ValidString(filename) {
  1112  		return url.PathEscape(filename)
  1113  	}
  1114  	for _, r := range filename {
  1115  		if unicode.Is(unicode.C, r) {
  1116  			safeFilename += url.PathEscape(string(r))
  1117  		} else {
  1118  			safeFilename += string(r)
  1119  		}
  1120  	}
  1121  	return safeFilename
  1122  }
  1123  
  1124  func GetSafePath(path string) (safePath string) {
  1125  	dir, file := filepath.Split(path)
  1126  	return filepath.Join(dir, GetSafeFilename(file))
  1127  }
  1128  
  1129  func FindFilePathWithNumberSuffix(parentDir string, basename string, useArbitraryName bool) (filePath string, err error) {
  1130  	ext := filepath.Ext(basename)
  1131  	if useArbitraryName {
  1132  		return filepath.Join(parentDir, strconv.FormatInt(time.Now().UnixNano(), 16)+ext), nil
  1133  	}
  1134  	destPath := filepath.Join(parentDir, basename)
  1135  	basename = basename[:len(basename)-len(ext)]
  1136  	// keep a sane limit on the loop.
  1137  	for suffix := 1; suffix < 100000; suffix++ {
  1138  		_, err := os.Stat(destPath)
  1139  		if os.IsNotExist(err) {
  1140  			break
  1141  		}
  1142  		if err != nil {
  1143  			return "", err
  1144  		}
  1145  		destPath = filepath.Join(parentDir, fmt.Sprintf("%s (%d)%s", basename, suffix, ext))
  1146  	}
  1147  	// Could race but it should be rare enough so fine.
  1148  	return destPath, nil
  1149  }
  1150  
  1151  func JsonwStringArray(a []string) *jsonw.Wrapper {
  1152  	aj := jsonw.NewArray(len(a))
  1153  	for i, s := range a {
  1154  		_ = aj.SetIndex(i, jsonw.NewString(s))
  1155  	}
  1156  	return aj
  1157  }
  1158  
  1159  var throttleBatchClock = clockwork.NewRealClock()
  1160  
  1161  type throttleBatchEmpty struct{}
  1162  
  1163  func isEmptyThrottleData(arg interface{}) bool {
  1164  	_, ok := arg.(throttleBatchEmpty)
  1165  	return ok
  1166  }
  1167  
  1168  func ThrottleBatch(f func(interface{}), batcher func(interface{}, interface{}) interface{},
  1169  	reset func() interface{}, delay time.Duration, leadingFire bool) (func(interface{}), func()) {
  1170  	var lock sync.Mutex
  1171  	var closeLock sync.Mutex
  1172  	var lastCalled time.Time
  1173  	var creation func(interface{})
  1174  	hasStored := false
  1175  	scheduled := false
  1176  	stored := reset()
  1177  	cancelCh := make(chan struct{})
  1178  	closed := false
  1179  	creation = func(arg interface{}) {
  1180  		lock.Lock()
  1181  		defer lock.Unlock()
  1182  		elapsed := throttleBatchClock.Since(lastCalled)
  1183  		isEmpty := isEmptyThrottleData(arg)
  1184  		leading := leadingFire || hasStored
  1185  		if !isEmpty {
  1186  			stored = batcher(stored, arg)
  1187  			hasStored = true
  1188  		}
  1189  		if elapsed > delay && (!isEmpty || hasStored) && leading {
  1190  			f(stored)
  1191  			stored = reset()
  1192  			hasStored = false
  1193  			lastCalled = throttleBatchClock.Now()
  1194  		} else if !scheduled && !isEmpty {
  1195  			scheduled = true
  1196  			go func() {
  1197  				select {
  1198  				case <-throttleBatchClock.After(delay - elapsed):
  1199  					lock.Lock()
  1200  					scheduled = false
  1201  					lock.Unlock()
  1202  					creation(throttleBatchEmpty{})
  1203  				case <-cancelCh:
  1204  					return
  1205  				}
  1206  			}()
  1207  		}
  1208  	}
  1209  	return creation, func() {
  1210  		closeLock.Lock()
  1211  		defer closeLock.Unlock()
  1212  		if closed {
  1213  			return
  1214  		}
  1215  		closed = true
  1216  		close(cancelCh)
  1217  	}
  1218  }
  1219  
  1220  // Format a proof for web-of-trust. Does not support all proof types.
  1221  func NewWotProof(proofType keybase1.ProofType, key, value string) (res keybase1.WotProof, err error) {
  1222  	switch proofType {
  1223  	case keybase1.ProofType_TWITTER, keybase1.ProofType_GITHUB, keybase1.ProofType_REDDIT,
  1224  		keybase1.ProofType_COINBASE, keybase1.ProofType_HACKERNEWS, keybase1.ProofType_FACEBOOK,
  1225  		keybase1.ProofType_GENERIC_SOCIAL, keybase1.ProofType_ROOTER:
  1226  		return keybase1.WotProof{
  1227  			ProofType: proofType,
  1228  			Name:      key,
  1229  			Username:  value,
  1230  		}, nil
  1231  	case keybase1.ProofType_GENERIC_WEB_SITE:
  1232  		return keybase1.WotProof{
  1233  			ProofType: proofType,
  1234  			Protocol:  key,
  1235  			Hostname:  value,
  1236  		}, nil
  1237  	case keybase1.ProofType_DNS:
  1238  		return keybase1.WotProof{
  1239  			ProofType: proofType,
  1240  			Protocol:  key,
  1241  			Domain:    value,
  1242  		}, nil
  1243  	default:
  1244  		return res, fmt.Errorf("unexpected proof type: %v", proofType)
  1245  	}
  1246  }
  1247  
  1248  // Format a web-of-trust proof for gui display.
  1249  func NewWotProofUI(mctx MetaContext, proof keybase1.WotProof) (res keybase1.WotProofUI, err error) {
  1250  	iconKey := ProofIconKey(mctx, proof.ProofType, proof.Name)
  1251  	res = keybase1.WotProofUI{
  1252  		SiteIcon:         MakeProofIcons(mctx, iconKey, ProofIconTypeSmall, 16),
  1253  		SiteIconDarkmode: MakeProofIcons(mctx, iconKey, ProofIconTypeSmallDarkmode, 16),
  1254  	}
  1255  	switch proof.ProofType {
  1256  	case keybase1.ProofType_TWITTER, keybase1.ProofType_GITHUB, keybase1.ProofType_REDDIT,
  1257  		keybase1.ProofType_COINBASE, keybase1.ProofType_HACKERNEWS, keybase1.ProofType_FACEBOOK,
  1258  		keybase1.ProofType_GENERIC_SOCIAL, keybase1.ProofType_ROOTER:
  1259  		res.Type = proof.Name
  1260  		res.Value = proof.Username
  1261  	case keybase1.ProofType_GENERIC_WEB_SITE:
  1262  		res.Type = proof.Protocol
  1263  		res.Value = proof.Hostname
  1264  	case keybase1.ProofType_DNS:
  1265  		res.Type = "dns"
  1266  		res.Value = proof.Domain
  1267  	default:
  1268  		return res, fmt.Errorf("unexpected proof type: %v", proof.ProofType)
  1269  	}
  1270  	return res, nil
  1271  }
  1272  
  1273  func ProofIconKey(mctx MetaContext, proofType keybase1.ProofType, genericKeyAndFallback string) (iconKey string) {
  1274  	switch proofType {
  1275  	case keybase1.ProofType_TWITTER:
  1276  		return "twitter"
  1277  	case keybase1.ProofType_GITHUB:
  1278  		return "github"
  1279  	case keybase1.ProofType_REDDIT:
  1280  		return "reddit"
  1281  	case keybase1.ProofType_HACKERNEWS:
  1282  		return "hackernews"
  1283  	case keybase1.ProofType_FACEBOOK:
  1284  		return "facebook"
  1285  	case keybase1.ProofType_GENERIC_SOCIAL:
  1286  		serviceType := mctx.G().GetProofServices().GetServiceType(mctx.Ctx(), genericKeyAndFallback)
  1287  		if serviceType != nil {
  1288  			return serviceType.GetLogoKey()
  1289  		}
  1290  		return genericKeyAndFallback
  1291  	case keybase1.ProofType_GENERIC_WEB_SITE, keybase1.ProofType_DNS:
  1292  		return "web"
  1293  	default:
  1294  		return genericKeyAndFallback
  1295  	}
  1296  }