all repos — safe @ 67378187b415bdee7c5a15a468a0a5829c8306ee

easily encrypt and decrypt in go

initial commit
Marco Andronaco andronacomarco@gmail.com
Fri, 14 Jun 2024 14:16:50 +0200
commit

67378187b415bdee7c5a15a468a0a5829c8306ee

5 files changed, 210 insertions(+), 0 deletions(-)

jump to
A LICENSE

@@ -0,0 +1,21 @@

+MIT License + +Copyright (c) 2024 Marco Andronaco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A README

@@ -0,0 +1,23 @@

+safe +---- + +A simple and intuitive way to encrypt and decrypt sensitive data. + + +USAGE + +Use NewSafe to create a new entity. You can provide a 32 bytes key or get a random one created. +If you use a random key, you won't be able to decrypt your data if you lose access to the Safe +entity or the program is restarted, so it's very recommended you provide your own key. + +Check out 'safe_test.go' for some examples. + + +INSTALLING + +go get github.com/BiRabittoh/safe + + +LICENSE + +safe is licensed under MIT.
A go.mod

@@ -0,0 +1,3 @@

+module github.com/BiRabittoh/safe + +go 1.22.4
A safe.go

@@ -0,0 +1,95 @@

+package safe + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "errors" +) + +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var ( + charsetLength = len(charset) +) + +type Safe struct { + c cipher.Block +} + +func get32ByteString() string { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + + passphrase := make([]byte, 32) + for i := 0; i < 32; i++ { + passphrase[i] = charset[int(randomBytes[i])%charsetLength] + } + return string(passphrase) + +} + +func NewSafe(password string) *Safe { + if len(password) != 32 { + println("WARNING: Using a random passphrase. Please use a fixed passphrase for production use.") + password = get32ByteString() + } + + key := []byte(password) + c, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + return &Safe{c} +} + +func (s *Safe) Encrypt(plaintext string) string { + blockSize := s.c.BlockSize() + plaintextBytes := []byte(plaintext) + + // PKCS#7 padding + padding := blockSize - len(plaintextBytes)%blockSize + padtext := append(plaintextBytes, bytes.Repeat([]byte{byte(padding)}, padding)...) + + ciphertext := make([]byte, blockSize+len(padtext)) + iv := ciphertext[:blockSize] + if _, err := rand.Read(iv); err != nil { + panic(err) + } + + mode := cipher.NewCBCEncrypter(s.c, iv) + mode.CryptBlocks(ciphertext[blockSize:], padtext) + + return hex.EncodeToString(ciphertext) +} + +func (s *Safe) Decrypt(ciphertextHex string) (string, error) { + ciphertext, err := hex.DecodeString(ciphertextHex) + if err != nil { + return "", err + } + + blockSize := s.c.BlockSize() + if len(ciphertext)%blockSize != 0 { + return "", errors.New("ciphertext is not a multiple of the block size") + } + + iv := ciphertext[:blockSize] + ciphertext = ciphertext[blockSize:] + + mode := cipher.NewCBCDecrypter(s.c, iv) + mode.CryptBlocks(ciphertext, ciphertext) + + // Remove padding + padding := int(ciphertext[len(ciphertext)-1]) + if padding > blockSize || padding <= 0 { + return "", errors.New("invalid padding") + } + + return string(ciphertext[:len(ciphertext)-padding]), nil +}
A safe_test.go

@@ -0,0 +1,68 @@

+package safe + +import ( + "testing" +) + +func TestSafeEncryptDecrypt(t *testing.T) { + // Test data + password := "thisis32bitlongpassphraseimusing" // Example 32-byte password + plaintext := "This is a secret" + + // Initialize Safe instance with a specific password + safe := NewSafe(password) + + // Encrypt plaintext + ciphertextHex := safe.Encrypt(plaintext) + + // Decrypt ciphertext + decryptedPlaintext, err := safe.Decrypt(ciphertextHex) + if err != nil { + t.Error(err) + } + + // Verify if the decrypted plaintext matches the original plaintext + if plaintext != decryptedPlaintext { + t.Errorf("Decrypt(Encrypt(%s)) = %s; want %s", plaintext, decryptedPlaintext, plaintext) + } +} + +func TestSafeWithRandomPassword(t *testing.T) { + // Initialize Safe instance with a random password + safe := NewSafe("") + + // Test plaintext + plaintext := "Hello, world!" + + // Encrypt plaintext + ciphertextHex := safe.Encrypt(plaintext) + + // Decrypt ciphertext + decryptedPlaintext, err := safe.Decrypt(ciphertextHex) + if err != nil { + t.Error(err) + } + + // Verify if the decrypted plaintext matches the original plaintext + if decryptedPlaintext != plaintext { + t.Errorf("Decrypt(Encrypt(%s)) = %s; want %s", plaintext, decryptedPlaintext, plaintext) + } +} + +func TestSafeWithBadInput(t *testing.T) { + // Test data + password := "thisis32bitlongpassphraseimusing" // Example 32-byte password + plaintext := "This is a secret" + + // Initialize Safe instance with a specific password + safe := NewSafe(password) + + // Encrypt plaintext + ciphertextHex := safe.Encrypt(plaintext) + + // Decrypt ciphertext + _, err := safe.Decrypt(ciphertextHex + ".") + if err == nil { + t.Errorf("Decrypt(Encrypt(%s)+\".\") did not return an error", plaintext) + } +}