DEV Community

IamKhan
IamKhan

Posted on

Let's build a password generator we can actually use

For our next beginner project, we will build a password generator that doesn't only generate passwords but encrypts and saves them - so that it is actually functional.

We will be splitting our code into different files so we don't end up with a large "main.go" file.

First, we initialize a go project and create a "profile.go" file that will contain the logic for encrypting and decrypting passwords.

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "errors"
    "io"
)

// must be 32 characters 
var key = "askdjasjdbreonfsdfibsdhfgsdfhboo"

var ErrMalformedEncryption = errors.New("malformed encryption")

// password in small letters so it is not stored
type profile struct {
    Enc, Platform, password string
}

func (p *profile) encrypt() error {
    block, err := aes.NewCipher([]byte(key))
    if err != nil {
        return err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return err
    }

    nonce := make([]byte, gcm.NonceSize())

    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return err
    }

    enc := gcm.Seal(nonce, nonce, []byte(p.password), nil)
    p.Enc = hex.EncodeToString(enc)

    return nil
}

func (p *profile) decrypt() error {
    block, err := aes.NewCipher([]byte(key))
    if err != nil {
        return err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return err
    }

    nsize := gcm.NonceSize()

    if len(p.Enc) < nsize {
        return ErrMalformedEncryption
    }

    enc, err := hex.DecodeString(p.Enc)
    if err != nil {
        return err
    }

    password, err := gcm.Open(nil, enc[:nsize], enc[nsize:], nil)
    if err != nil {
        return err
    }

    p.password = string(password)

    return nil
}

Enter fullscreen mode Exit fullscreen mode

Here we create a profile struct that has 3 fields - Enc, Platform and password. Enc will hold the encrypted password, the service we are generating the password for will be stored in Platform and password will hold the actual generated password. The profile struct has 2 methods "encrypt" and "decrypt". We use AES - a symmetric key encryption algorithm to encrypt and decrypt our password.

Next we create a "store.go" file that contains the logic for storing and retrieving passwords.

package main

import (
    "encoding/gob"
    "errors"
    "os"
    "sync"
)

const filename = "profile.bin"

var (
    ErrInvalidArgs = errors.New("invalid args")
    ErrNotFound    = errors.New("not found")
)

type store struct {
    sync.RWMutex
    data map[string]*profile
}

func newStore() (*store, error) {
    s := &store{
        data: make(map[string]*profile),
    }

    if err := s.load(); err != nil {
        return nil, err
    }

    return s, nil
}

func (s *store) load() error {
    flags := os.O_CREATE | os.O_RDONLY
    f, err := os.OpenFile(filename, flags, 0644)
    if err != nil {
        return err
    }
    defer f.Close()

    info, err := f.Stat()
    if err != nil {
        return err
    }

    if info.Size() == 0 {
        return nil
    }

    return gob.NewDecoder(f).Decode(&s.data)
}

func (s *store) save() error {
    f, err := os.OpenFile(filename, os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer f.Close()

    return gob.NewEncoder(f).Encode(s.data)
}

func (s *store) find(platform string) (string, error) {
    s.RLock()
    defer s.RUnlock()

    p, ok := s.data[platform]
    if !ok {
        return "", ErrNotFound
    }

    if err := p.decrypt(); err != nil {
        return "", err
    }

    return p.password, nil
}

func (s *store) add(platform, password string) error {
    if platform == "" {
        return ErrInvalidArgs
    }

    p := &profile{
        Platform: platform,
        password: password,
    }

    if err := p.encrypt(); err != nil {
        return err
    }

    s.Lock()
    defer s.Unlock()

    s.data[platform] = p

    return s.save()
}
Enter fullscreen mode Exit fullscreen mode

We chose gob files for storage because they are not exactly human readable. If the file ever gets exposed, your passwords are safe because they will be encrypted and really difficult to read. The store struct contains methods for loading, finding and saving to the gob file. We save the passwords in a dictionary. We also use a mutex to make the dictionary concurrent safe. An important thing to note is that we will not be storing the plain generated password - we will store its encrypted value instead.

Now let's write a couple of functions that will actually generate the passwords. Create a "password.go" file and type the following

package main

import (
    "math"
    "math/rand"
    "slices"
    "strings"
)

const (
    half      = .5
    onethird  = .3
    onefourth = .25
)

var (
    randlowers  = randFromSeed(lowers())
    randuppers  = randFromSeed(uppers())
    randdigits  = randFromSeed(digits())
    randsymbols = randFromSeed(symbols())
)

var basicPassword = randlowers

func mediumPassword(n int) string {
    frac := math.Round(float64(n) * half)
    pwd := basicPassword(n)
    return pwd[:n-int(frac)] + randuppers(int(frac))
}

func hardPassword(n int) string {
    pwd := mediumPassword(n)
    frac := math.Round(float64(n) * onethird)
    return pwd[:n-int(frac)] + randdigits(int(frac))
}

func xhardPassword(n int) string {
    pwd := hardPassword(n)
    frac := math.Round(float64(n) * onefourth)
    return pwd[:n-int(frac)] + randsymbols(int(frac))
}

func randFromSeed(seed string) func(int) string {
    return func(n int) string {
        var b strings.Builder
        for range n {
            b.WriteByte(seed[rand.Intn(len(seed))])
        }
        return b.String()
    }
}

func lowers() string {
    var b strings.Builder
    for i := 'a'; i < 'a'+26; i++ {
        b.WriteRune(i)
    }
    return b.String()
}

func uppers() string {
    var b strings.Builder
    for i := 'A'; i < 'A'+26; i++ {
        b.WriteRune(i)
    }
    return b.String()
}

func symbols() string {
    var b strings.Builder
    for i := '!'; i < '!'+14; i++ {
        b.WriteRune(i)
    }
    for i := ':'; i < ':'+6; i++ {
        b.WriteRune(i)
    }
    for i := '['; i < '['+5; i++ {
        b.WriteRune(i)
    }
    for i := '{'; i < '{'+3; i++ {
        b.WriteRune(i)
    }
    return b.String()
}

func digits() string {
    var b strings.Builder
    for i := '0'; i < '0'+9; i++ {
        b.WriteRune(i)
    }
    return b.String()
}

func shuffle[T any](ts []T) []T {
    cloned := slices.Clone(ts)
    rand.Shuffle(len(cloned), func(i, j int) {
        cloned[i], cloned[j] = cloned[j], cloned[i]
    })
    return cloned
}

func shuffleStr(s string) string {
    return strings.Join(shuffle(strings.Split(s, "")), "")
}

Enter fullscreen mode Exit fullscreen mode

Here we've written functions that generates passwords at different levels of difficulty. The basicPassword function generates random small letter strings. The mediumPassword function takes a fraction of the characters from basicPassword function and adds random capital letters to it. The hardPassword function does the same for mediumPassword but adds digits to it. The xhardPassword does the same and adds symbols. The shuffle function does exactly what you'd expect on slices while shuffleStr shuffles strings.

Now let's put everything together. Create a "main.go" file and type the following

package main

import (
    "errors"
    "flag"
    "fmt"
    "log"
    "regexp"
    "strconv"
    "strings"
)

var usage = `
Usage 
-----
--get platform=[string] - Gets saved password for a platform
--set platform=[string] len=[int] level=(basic|medium|hard|xhard) - Creates and saves a password
`

var ErrUsage = errors.New(usage)

var pattern = regexp.MustCompile(`\S+=\S+`)

type level int

const (
    _ level = iota
    level_basic
    level_medium
    level_hard
    level_xhard
)

var level_key = map[string]level{
    "basic":  level_basic,
    "medium": level_medium,
    "hard":   level_hard,
    "xhard":  level_xhard,
}

type commands struct {
    get, set bool
}

func createCommands() (c commands) {
    flag.BoolVar(&c.get, "get", false, "get password for platform")
    flag.BoolVar(&c.set, "set", false, "set password for platform")
    flag.Parse()
    return
}

func (c commands) exec(store *store) (string, error) {
    switch {
    case c.get:
        return c.getPassword(store)
    case c.set:
        return c.setPassword(store)
    default:
        return "", ErrUsage
    }
}

func (c commands) getPassword(store *store) (string, error) {
    params, err := c.parse()
    if err != nil {
        return "", err
    }

    return store.find(params["platform"])
}

func (c commands) setPassword(store *store) (string, error) {
    params, err := c.parse()
    if err != nil {
        return "", err
    }

    var password string

    n, err := strconv.Atoi(params["len"])
    if err != nil {
        return "", err
    }

    if n < 8 {
        return "", fmt.Errorf("password len cannot be less than 8")
    }

    switch level_key[params["level"]] {
    case level_basic:
        password = basicPassword(n)
    case level_medium:
        password = mediumPassword(n)
    case level_hard:
        password = hardPassword(n)
    case level_xhard:
        password = xhardPassword(n)
    default:
        return "", ErrUsage
    }

    password = shuffleStr(password)

    if err := store.add(params["platform"], password); err != nil {
        return "", err
    }

    return password, nil
}

func (c commands) parse() (map[string]string, error) {
    args := flag.Args()

    if len(args) == 0 {
        return nil, ErrUsage
    }

    params := make(map[string]string)

    for i := range args {
        if !pattern.MatchString(args[i]) {
            return nil, ErrUsage
        }

        parts := strings.Split(args[i], "=")
        params[parts[0]] = parts[1]
    }

    return params, nil
}

func main() {
    store, err := newStore()
    if err != nil {
        log.Fatalf("could not initialize store: %v", err)
    }

    c := createCommands()

    password, err := c.exec(store)
    if err != nil {
        log.Fatalf("could not execute flag commands: %v", err)
    }

    fmt.Printf("password: %s\n", password)
}
Enter fullscreen mode Exit fullscreen mode

We used flags to specify how we expect the application to behave. "--get" to get a password and "--set" to generate and save a password. To set a password, the user provides arguments with the flags to instruct the application on the type of password to generate and save. To get a password, the user also provides arguments to specify the password to retrieve.

You can now run "go build" to build a binary and test the application.

password-generator-demo

Top comments (0)