github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/secret_store_file.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  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  )
    12  
    13  type secretBytes [LKSecLen]byte
    14  
    15  func NewErrSecretForUserNotFound(username NormalizedUsername) SecretStoreError {
    16  	return SecretStoreError{
    17  		Msg: fmt.Sprintf("No secret found for %s", username),
    18  	}
    19  }
    20  
    21  type SecretStoreFile struct {
    22  	dir          string
    23  	notifyCreate func(NormalizedUsername)
    24  }
    25  
    26  var _ SecretStoreAll = (*SecretStoreFile)(nil)
    27  
    28  func NewSecretStoreFile(dir string) *SecretStoreFile {
    29  	return &SecretStoreFile{dir: dir}
    30  }
    31  
    32  func (s *SecretStoreFile) RetrieveSecret(mctx MetaContext, username NormalizedUsername) (secret LKSecFullSecret, err error) {
    33  	defer mctx.Trace(fmt.Sprintf("SecretStoreFile.RetrieveSecret(%s)", username), &err)()
    34  	mctx.Debug("Retrieving secret V2 from file")
    35  	secret, err = s.retrieveSecretV2(mctx, username)
    36  	switch err.(type) {
    37  	case nil:
    38  		return secret, nil
    39  	case SecretStoreError:
    40  	default:
    41  		return LKSecFullSecret{}, err
    42  	}
    43  
    44  	// check for v1
    45  	mctx.Debug("Retrieving secret V1 from file")
    46  	secret, err = s.retrieveSecretV1(mctx, username)
    47  	if err != nil {
    48  		return LKSecFullSecret{}, err
    49  	}
    50  
    51  	// upgrade to v2
    52  	if err = s.StoreSecret(mctx, username, secret); err != nil {
    53  		return secret, err
    54  	}
    55  	if err = s.clearSecretV1(username); err != nil {
    56  		return secret, err
    57  	}
    58  	return secret, nil
    59  }
    60  
    61  func (s *SecretStoreFile) retrieveSecretV1(mctx MetaContext, username NormalizedUsername) (LKSecFullSecret, error) {
    62  	userpath := s.userpath(username)
    63  	mctx.Debug("SecretStoreFile.retrieveSecretV1: checking path: %s", userpath)
    64  	secret, err := os.ReadFile(userpath)
    65  	if err != nil {
    66  		if os.IsNotExist(err) {
    67  			return LKSecFullSecret{}, NewErrSecretForUserNotFound(username)
    68  		}
    69  
    70  		return LKSecFullSecret{}, err
    71  	}
    72  
    73  	return newLKSecFullSecretFromBytes(secret)
    74  }
    75  
    76  func (s *SecretStoreFile) retrieveSecretV2(mctx MetaContext, username NormalizedUsername) (LKSecFullSecret, error) {
    77  	userpath := s.userpathV2(username)
    78  	mctx.Debug("SecretStoreFile.retrieveSecretV2: checking path: %s", userpath)
    79  	xor, err := os.ReadFile(userpath)
    80  	if err != nil {
    81  		if os.IsNotExist(err) {
    82  			return LKSecFullSecret{}, NewErrSecretForUserNotFound(username)
    83  		}
    84  
    85  		return LKSecFullSecret{}, err
    86  	}
    87  
    88  	noise, err := os.ReadFile(s.noisepathV2(username))
    89  	if err != nil {
    90  		if os.IsNotExist(err) {
    91  			return LKSecFullSecret{}, NewErrSecretForUserNotFound(username)
    92  		}
    93  
    94  		return LKSecFullSecret{}, err
    95  	}
    96  
    97  	var xorFixed secretBytes
    98  	copy(xorFixed[:], xor)
    99  	var noiseFixed NoiseBytes
   100  	copy(noiseFixed[:], noise)
   101  	secret, err := NoiseXOR(xorFixed, noiseFixed)
   102  	if err != nil {
   103  		return LKSecFullSecret{}, err
   104  	}
   105  
   106  	return newLKSecFullSecretFromBytes(secret)
   107  }
   108  
   109  func (s *SecretStoreFile) StoreSecret(mctx MetaContext, username NormalizedUsername, secret LKSecFullSecret) (err error) {
   110  	defer mctx.Trace(fmt.Sprintf("SecretStoreFile.StoreSecret(%s)", username), &err)()
   111  	noise, err := MakeNoise()
   112  	if err != nil {
   113  		return err
   114  	}
   115  	var secretFixed secretBytes
   116  	copy(secretFixed[:], secret.Bytes())
   117  	xor, err := NoiseXOR(secretFixed, noise)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	if err := os.MkdirAll(s.dir, PermDir); err != nil {
   123  		return err
   124  	}
   125  
   126  	fsec, err := os.CreateTemp(s.dir, username.String())
   127  	if err != nil {
   128  		return err
   129  	}
   130  	fnoise, err := os.CreateTemp(s.dir, username.String())
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	// remove the temp file if it still exists at the end of this function
   136  	defer func() { _ = ShredFile(fsec.Name()) }()
   137  	defer func() { _ = ShredFile(fnoise.Name()) }()
   138  
   139  	if runtime.GOOS != "windows" {
   140  		// os.Fchmod not supported on windows
   141  		if err := fsec.Chmod(PermFile); err != nil {
   142  			return err
   143  		}
   144  		if err := fnoise.Chmod(PermFile); err != nil {
   145  			return err
   146  		}
   147  	}
   148  	if _, err := fsec.Write(xor); err != nil {
   149  		return err
   150  	}
   151  	if err := fsec.Close(); err != nil {
   152  		return err
   153  	}
   154  	if _, err := fnoise.Write(noise[:]); err != nil {
   155  		return err
   156  	}
   157  	if err := fnoise.Close(); err != nil {
   158  		return err
   159  	}
   160  
   161  	finalSec := s.userpathV2(username)
   162  	finalNoise := s.noisepathV2(username)
   163  
   164  	exists, err := FileExists(finalSec)
   165  	if err != nil {
   166  		return err
   167  	}
   168  
   169  	// NOTE: Pre-existing maybe-bug: I think this step breaks atomicity. It's
   170  	// possible that the rename below fails, in which case we'll have already
   171  	// destroyed the previous value.
   172  
   173  	// On Unix we could solve this by hard linking the old file to a new tmp
   174  	// location, and then shredding it after the rename. On Windows, I think
   175  	// we'd need to somehow call the ReplaceFile Win32 function (which Go
   176  	// doesn't expose anywhere as far as I know, so this would require CGO) to
   177  	// take advantage of its lpBackupFileName param.
   178  	if exists {
   179  		// shred the existing secret
   180  		if err := s.clearSecretV2(username); err != nil {
   181  			return err
   182  		}
   183  	}
   184  
   185  	if err := os.Rename(fsec.Name(), finalSec); err != nil {
   186  		return err
   187  	}
   188  	if err := os.Rename(fnoise.Name(), finalNoise); err != nil {
   189  		return err
   190  	}
   191  
   192  	if err := os.Chmod(finalSec, PermFile); err != nil {
   193  		return err
   194  	}
   195  	if err := os.Chmod(finalNoise, PermFile); err != nil {
   196  		return err
   197  	}
   198  
   199  	// if we just created the secret store file for the
   200  	// first time, notify anyone interested.
   201  	if !exists && s.notifyCreate != nil {
   202  		s.notifyCreate(username)
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func (s *SecretStoreFile) ClearSecret(mctx MetaContext, username NormalizedUsername) (err error) {
   209  	defer mctx.Trace(fmt.Sprintf("SecretStoreFile.ClearSecret(%s)", username), &err)()
   210  	// try both
   211  
   212  	if username.IsNil() {
   213  		mctx.Debug("NOOPing SecretStoreFile#ClearSecret for empty username")
   214  		return nil
   215  	}
   216  
   217  	errV1 := s.clearSecretV1(username)
   218  	errV2 := s.clearSecretV2(username)
   219  
   220  	err = CombineErrors(errV1, errV2)
   221  	if err != nil {
   222  		mctx.Debug("Failed to clear secret in at least one version: %s", err)
   223  		return err
   224  	}
   225  	return nil
   226  }
   227  
   228  func (s *SecretStoreFile) clearSecretV1(username NormalizedUsername) error {
   229  	if err := ShredFile(s.userpath(username)); err != nil {
   230  		if os.IsNotExist(err) {
   231  			return nil
   232  		}
   233  		return err
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func (s *SecretStoreFile) clearSecretV2(username NormalizedUsername) error {
   240  	exists, err := FileExists(s.noisepathV2(username))
   241  	if err != nil {
   242  		return err
   243  	}
   244  	if !exists {
   245  		return nil
   246  	}
   247  
   248  	nerr := ShredFile(s.noisepathV2(username))
   249  	uerr := ShredFile(s.userpathV2(username))
   250  	if nerr != nil {
   251  		return nerr
   252  	}
   253  	if uerr != nil {
   254  		return uerr
   255  	}
   256  	return nil
   257  }
   258  
   259  func (s *SecretStoreFile) GetUsersWithStoredSecrets(mctx MetaContext) (users []string, err error) {
   260  	defer mctx.Trace("SecretStoreFile.GetUsersWithStoredSecrets", &err)()
   261  	files, err := filepath.Glob(filepath.Join(s.dir, "*.ss*"))
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	users = make([]string, 0, len(files))
   266  	for _, f := range files {
   267  		uname := stripExt(filepath.Base(f))
   268  		if !isPPSSecretStore(uname) {
   269  			users = append(users, uname)
   270  		}
   271  	}
   272  	return users, nil
   273  }
   274  
   275  func (s *SecretStoreFile) userpath(username NormalizedUsername) string {
   276  	return filepath.Join(s.dir, fmt.Sprintf("%s.ss", username))
   277  }
   278  
   279  func (s *SecretStoreFile) userpathV2(username NormalizedUsername) string {
   280  	return filepath.Join(s.dir, fmt.Sprintf("%s.ss2", username))
   281  }
   282  
   283  func (s *SecretStoreFile) noisepathV2(username NormalizedUsername) string {
   284  	return filepath.Join(s.dir, fmt.Sprintf("%s.ns2", username))
   285  }
   286  
   287  func (s *SecretStoreFile) GetOptions(MetaContext) *SecretStoreOptions  { return nil }
   288  func (s *SecretStoreFile) SetOptions(MetaContext, *SecretStoreOptions) {}
   289  
   290  func stripExt(path string) string {
   291  	for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- {
   292  		if path[i] == '.' {
   293  			return path[:i]
   294  		}
   295  	}
   296  	return ""
   297  }