go |gofr

Building a Playful File Locker with GoFr

ssshekhu53

Shashank Shekhar

Posted on April 19, 2024

Building a Playful File Locker with GoFr

Forget complex permission schemes, sometimes you just want a quick and dirty way to lock a file. This guide takes you on a fun adventure to create a basic file locker command-line tool using GoFr, specifically focusing on understanding CLI tool development rather than building a production-ready application.

What is GoFr?

GoFr is an opinionated Go framework for accelerated microservice development. It takes an "opinionated" approach, meaning it has a specific way of doing things that streamlines development. This makes Gofr ideal for creating robust and scalable web applications without a lot of boilerplate code.

Gofr is designed to be familiar and user-friendly for developers, even those new to Go. It provides a clear and consistent way to structure your web application, making it easier to understand and maintain.

Building Our Playful File Locker

1. Setting the Stage:

  • Make sure you have Go installed https://go.dev/.
  • Create a project directory and initialize a Go module within it using go mod init . Replace with a fun name for your locker tool!
  • Add gofr package to the project using the following command: go get gofr.dev

2. Directory structure

├── file-locker
│   ├── handlers
│   │   ├── unix
│   │   │   ├── handler.go
│   ├── services
│   │   ├── unix
│   │   │   ├── service.go
│   │   ├── crypt
│   │   │   ├── crypt.go
│   ├── constants
│   │   ├── contants.go
│   ├── main.go
│   ├── go.mod
Enter fullscreen mode Exit fullscreen mode

3. Service Layer

  • Let's add interface for the service. Add following code to services/interfaces.go:

    type FileLocker interface {  
        Init(password string) error  
        Lock() error  
        Unlock(password string) error  
    }
    
    type Crypt interface {  
        Encrypt(creds []byte) []byte  
        Decrypt(cred string) ([]byte, error)  
    }
    
  • Let's add implementation for interfaces. Add following code to services/unix/service.go:

  const (  
     fileName = `private`  
     hiddenFileName = `.private`  
     encryptedDataFileName = `.encrypted-data`  
  )  

  type unix struct {  
     crypt services.Crypt  
  }  

  func New(crypt services.Crypt) services.FileLocker {  
     return &unix{crypt: crypt}  
  }  

  func (m *unix) Init(password string) error {  
     _, err := os.Stat(fileName)  
     if err == nil {  
        return errors.New("file locker already initialized")  
     }  

     err = os.Mkdir(fileName, os.ModePerm)  
     if err != nil {  
        return err  
     }  

     _, err = os.Create(filepath.Join(fileName, ".nomedia"))  
     if err != nil {  
        return err  
     }  

     encryptedPassword := m.crypt.Encrypt([]byte(password))  

     err = os.WriteFile(filepath.Join(fileName, encryptedDataFileName), encryptedPassword, os.ModePerm)  
     if err != nil {  
        return err  
     }  

     return nil  
  }  

  func (m *unix) Lock() error {  
     _, err := os.Stat(fileName)  
     if err != nil {  
        if err == os.ErrNotExist {  
           return errors.New("file locker not initialized or is still locked")  
        }  

        return err  
     }  

     return os.Rename(fileName, hiddenFileName)  
  }  

  func (m *unix) Unlock(password string) error {  
     _, err := os.Stat(hiddenFileName)  
     if err != nil {  
        if err == os.ErrNotExist {  
           return errors.New("file locker not initialized or is still unlocked")  
        }  

        return err  
     }  

     data, err := os.ReadFile(filepath.Join(hiddenFileName, encryptedDataFileName))  
     if err != nil {  
        return err  
     }  

     decryptedData, err := m.crypt.Decrypt(string(data))  
     if err != nil {  
        return err  
     }  

     if password != string(decryptedData) {  
        return errors.New("unauthorized")  
     }  

     return os.Rename(hiddenFileName, fileName)  
  }
Enter fullscreen mode Exit fullscreen mode
- Struct `unix` implements `FileLocker` interface.
- `New` is the factory function that returns a `FileLocker` instance
- `Init` method will create a directory named `private` in the current directory and will store the encrypted password in file named `.encrypted-data`. If `private` directory is already present or there occurs some error while creating it then `Init` method will return error otherwise will return nil.
- `Lock` method will hide the `private` directory by renaming it to `.private`. If the `private` directory is not present in the current directory then it means that either `file-locker` has not been initialised or it is already in locked state.
- `Unlock` method will read the password from `.private/.encrypted-data` file and compare with the password received in the parameter. If it matches, then it will unhide the `private` file otherwise return `unauthorized` error.
Enter fullscreen mode Exit fullscreen mode
  • Add the following code to services/crypt/crypt.go:
  const secret = `qwertyuiopasdfghjklzxcvb`  

  type crypt struct {  
     block cipher.Block  
  }  

  func New() (services.Crypt, error) {  
     block, err := aes.NewCipher([]byte(secret))  
     if err != nil {  
        return nil, err  
     }  

     return &crypt{block: block}, nil  
  }  

  func (c *crypt) Encrypt(data []byte) []byte {  
     ciphertext := make([]byte, aes.BlockSize+len(data))  
     iv := ciphertext[:aes.BlockSize]  

     cfb := cipher.NewCFBEncrypter(c.block, iv)  
     cipherText := make([]byte, len(data))  
     cfb.XORKeyStream(cipherText, data)  

     dst := make([]byte, base64.StdEncoding.EncodedLen(len(cipherText)))  

     base64.StdEncoding.Encode(dst, cipherText)  

     return dst  
  }  

  func (c *crypt) Decrypt(data string) ([]byte, error) {  
     ciphertext := make([]byte, aes.BlockSize+len(data))  

     iv := ciphertext[:aes.BlockSize]  
     cfb := cipher.NewCFBDecrypter(c.block, iv)  

     cipherText, err := base64.StdEncoding.DecodeString(data)  
     if err != nil {  
        return []byte{}, err  
     }  

     plainText := make([]byte, len(cipherText))  
     cfb.XORKeyStream(plainText, cipherText)  

     return plainText, nil  
  }
Enter fullscreen mode Exit fullscreen mode
- `Encrypt` method will encrypt the data given in the parameter and will return the encrypted string.
- `Decrypt` method will decrypt the string given in the parameter and will return the decrypted string.
- `secret` is a constant that has random string that will be used to encrypt and decrypt the data. You are free to update the value of secret but keeping a strong secret is advisable.
Enter fullscreen mode Exit fullscreen mode

4. Handler layer

  • Add the following code in handlers/handler.go:
  type Handler interface {  
     Init(ctx *gofr.Context) (interface{}, error)  
     Lock(ctx *gofr.Context) (interface{}, error)  
     Unlock(ctx *gofr.Context) (interface{}, error)  
     Help(_ *gofr.Context) (interface{}, error)  
  }  

  type handler struct {  
     service services.FileLocker  
  }  

  func New(service services.FileLocker) Handler {  
     return &handler{service: service}  
  }  

  func (h *handler) Init(ctx *gofr.Context) (interface{}, error) {  
     password := ctx.Param("password")  
     if password == "" {  
        return nil, errors.New("password is required")  
     }  

     err := h.service.Init(password)  
     if err != nil {  
        return nil, err  
     }  

     return "file locker initialized", nil  
  }  

  func (h *handler) Lock(_ *gofr.Context) (interface{}, error) {  
     err := h.service.Lock()  
     if err != nil {  
        return nil, err  
     }  

     return "file locked", nil  
  }  

  func (h *handler) Unlock(ctx *gofr.Context) (interface{}, error) {  
     password := ctx.Param("password")  
     if password == "" {  
        return nil, errors.New("password is required")  
     }  

     err := h.service.Unlock(password)  
     if err != nil {  
        return nil, err  
     }  

     return "file unlocked", nil  
  }  

  func (h *handler) Help(_ *gofr.Context) (interface{}, error) {  
     return `File Locker CLI Tool  

  Usage:  
   file-locker [command]  
  Available Commands:  
   init      Create a directory named private and initialize the file locker lock      Hide the private directory unlock    Unhides the private directory`, nil  
  }
Enter fullscreen mode Exit fullscreen mode
- `handler` is the struct that implements `Handler` interface.
- `Init` is the handler for `init` command. It takes `password` as the flag. The value for this flag will be used in unlocking.
- `Lock` method is the handler for `lock` command.
- `Unlock` method is the handler for `unlock` command. It takes `password` flag for password to unlock.
- `Help` method is the handler for `help` command. It returns the help string for the app.
Enter fullscreen mode Exit fullscreen mode

5. Main

  • Add the following code in main.go
  func main() {  
     var service services.FileLocker  

    app := gofr.NewCMD()  

     crypt, err := cryptPkg.New()  
     if err != nil {  
        app.Logger().Fatalf("Error occurred: %v", err)  
     }  

     switch runtime.GOOS {  
     case constants.Darwin, constants.Linux:  
        service = unix.New(crypt)  
     default:  
        app.Logger().Fatalf("Unsupported architecture: %v", runtime.GOOS)  
     }  

     handler := handlers.New(service)  

     app.SubCommand("init", handler.Init)  
     app.SubCommand("unlock", handler.Unlock)  
     app.SubCommand("lock", handler.Lock)  
     app.SubCommand("help", handler.Help)  

     app.Run()  
  }
Enter fullscreen mode Exit fullscreen mode
- `NewCMD()` function creates a command line application
- Depending upon the architecture of the underlying OS, we are creating service. This service will be injected in the handler. Currently, we have only implemented service layer for Unix based OS hence in the switch statement we only have case for `Darwin` (for MacOS) and `Linux`.
- `SubCommand` method adds a sub-command to the CLI application. Here, we have added four sub-commands, viz. `init`, `unlock`, `lock`, `help`. The second argument in the `SubCommand` method is the handler that will be called for execution when that particular sub-command is given.
- `Run()` method will run the application.
Enter fullscreen mode Exit fullscreen mode

Add constants in constants/constants.go and our application is ready for use.

const (  
   Darwin = `darwin`  
   Linux = `linux`  
)
Enter fullscreen mode Exit fullscreen mode

Compiling and Having Fun!

  • Compile the program using go build -o file-locker .
  • Run the program with the desired action (sub-command):

    ./file-locker <sub-command> [flags]
    
    • To create a private directory use:
      ./file-locker init -password=<password>
    

    Replace with your desired password. This will create a directory named private in the current directory. You can put all your files that you require to lock in this directory.

    • To lock (hide) the private directory, use:
      ./file-locker lock
    
    • To unlock (unhide) the private directory, use:
      ./file-locker unlock -password=<password>
    

Conclusion

Congratulations! You've built a basic file locker command-line tool in Go. It may not be production-ready, but it demonstrates some key concepts of file operations and CLI development using GoFr. While we focused on a playful implementation here, this knowledge can be applied to create more robust tools in the future.

Want to see the complete code and experiment further? The entire codebase for this playful file locker is available at file-locker. Feel free to clone it, explore the implementation, and make it your own!

Note: Currently, this implementation is designed for Unix-based systems. If you're adventurous, try extending it to support Windows by adding platform-specific logic for hiding and unhiding directories.

💖 💪 🙅 🚩
ssshekhu53
Shashank Shekhar

Posted on April 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related