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
}
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()
}
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, "")), "")
}
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)
}
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.
Top comments (0)