mirror of
https://github.com/rwinkhart/rcw.git
synced 2026-08-28 04:46:42 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e79adb82a0 | ||
|
|
e8ef243e87 | ||
|
|
ddd7f893e9 | ||
|
|
f49248930b | ||
|
|
1745b85662 | ||
|
|
c5e259a446 | ||
|
|
4fd72b0042 |
@@ -3,12 +3,13 @@ RCW is a cascading symmetric cryptography agent meant to be embedded within Go p
|
||||
|
||||
It encrypts all data with both AES256-GCM and ChaCha20-Poly1305.
|
||||
|
||||
Passphrases are securely cached for three minutes and RPC authentication is used to
|
||||
ensure that only the binary+user responsible for caching the passphrase can utilize it.
|
||||
This feature is supported on Linux, FreeBSD, MacOS, and Windows.
|
||||
Passwords are securely cached for three minutes and RPC authentication is used to
|
||||
ensure that only the binary+user responsible for caching the password can utilize it.
|
||||
This feature is supported on Linux, FreeBSD, MacOS, and Windows. It is disabled if
|
||||
built with `-tags=interactive`.
|
||||
|
||||
RCW also features a sanity check to ensure no data loss occurs due to a user entering the
|
||||
incorrect passphrase during encryption.
|
||||
incorrect password during encryption.
|
||||
|
||||
Please note that RCW is a work-in-progress and breaking changes should be expected.
|
||||
Future versions may not be capable of decrypting the output of the current version.
|
||||
@@ -20,3 +21,16 @@ Future versions may not be capable of decrypting the output of the current versi
|
||||
|
||||
# Usage
|
||||
For now, please reference [example.go](https://github.com/rwinkhart/randalls-cryptographic-wrappers/blob/main/example.go).
|
||||
|
||||
# IMPORTANT - READ FOR FreeBSD+Windows SUPPORT w/RCWD!
|
||||
There are replacements in the `go.mod` for this module. Make sure you maintain those
|
||||
same replacements in your importing module, otherwise FreeBSD and Windows support will break
|
||||
(RCWD support for those operating systems requires patched modules).
|
||||
|
||||
# runtime/secret.Do()?
|
||||
This module does not yet make use of the new `secret.Do()` function in Go 1.26.
|
||||
The new function is currently in an experimental state and does not function on
|
||||
non-Linux platforms, thus the current approach to zeroizing in-memory secrets is
|
||||
a manual one that will work everywhere. If `secret.Do()` becomes more complete in
|
||||
a future Go version, it will likely be adopted here (on top of the current manual
|
||||
approach).
|
||||
|
||||
+6
-5
@@ -11,16 +11,16 @@ import (
|
||||
var Timeout = 300 // seconds for RPC server timeout; configurable
|
||||
|
||||
var daemonHash []byte
|
||||
var globalPassphrase []byte
|
||||
var globalPassword []byte
|
||||
|
||||
// RCWService provides an RPC method.
|
||||
type RCWService struct{}
|
||||
|
||||
// DecryptRequest is the RPC method that decrypts the incoming data using
|
||||
// the global passphrase and returns the decrypted data
|
||||
// the global password and returns the decrypted data
|
||||
func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
|
||||
var err error
|
||||
*reply, err = wrappers.Decrypt(encBytes, globalPassphrase)
|
||||
*reply, err = wrappers.Decrypt(encBytes, globalPassword, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -28,15 +28,16 @@ func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
|
||||
}
|
||||
|
||||
// EncryptRequest is the RPC method that encrypts the incoming data using
|
||||
// the global passphrase and returns the encrypted data
|
||||
// the global password and returns the encrypted data
|
||||
func (h *RCWService) EncryptRequest(decBytes []byte, reply *[]byte) error {
|
||||
*reply = wrappers.Encrypt(decBytes, globalPassphrase)
|
||||
*reply = wrappers.Encrypt(decBytes, globalPassword, true, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFileHash returns the SHA256 hash of the file at the given path.
|
||||
func getFileHash(path string) []byte {
|
||||
file, _ := os.Open(path)
|
||||
defer file.Close()
|
||||
hash := sha256.New()
|
||||
io.Copy(hash, file)
|
||||
return hash.Sum(nil)
|
||||
|
||||
@@ -13,14 +13,15 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
peercred "github.com/rwinkhart/peercred-mini"
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
"github.com/rwinkhart/peercred-mini"
|
||||
)
|
||||
|
||||
// Start is the entry point for the RPC server responsible for
|
||||
// returning decrypted data to authenticated clients.
|
||||
func Start(passphrase []byte) {
|
||||
// store passphrase to be referenced by DecryptRequest method
|
||||
globalPassphrase = passphrase
|
||||
func Start(password []byte) {
|
||||
// store password to be referenced by DecryptRequest method
|
||||
globalPassword = password
|
||||
|
||||
// register RCWService with the RPC package
|
||||
if err := rpc.Register(&RCWService{}); err != nil {
|
||||
@@ -38,12 +39,13 @@ func Start(passphrase []byte) {
|
||||
defer listener.Close()
|
||||
log.Printf("RPC daemon listening on unix://%s", socketPath)
|
||||
|
||||
// capture sigterms to ensure listener is closed
|
||||
// capture termination signals to ensure listener is closed
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
listener.Close()
|
||||
security.ZeroizeBytes(globalPassword)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
@@ -56,6 +58,7 @@ func Start(passphrase []byte) {
|
||||
if err.(net.Error).Timeout() {
|
||||
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
|
||||
listener.Close()
|
||||
security.ZeroizeBytes(globalPassword)
|
||||
os.Exit(0)
|
||||
}
|
||||
log.Printf("Accept error: %v", err)
|
||||
@@ -69,9 +72,9 @@ func Start(passphrase []byte) {
|
||||
// handleConn verifies the identity of the client.
|
||||
// It uses the file descriptor of the connection to get the PID of the client,
|
||||
// which is then used to get the path of the client's executable and calculate its hash.
|
||||
// The passphrase is only returned if the client's executable hash matches the daemon's hash
|
||||
// The password is only returned if the client's executable hash matches the daemon's hash
|
||||
// and if the request is coming from the same user.
|
||||
// This ensures that only the binary the daemon is embedded in can retrieve the passphrase.
|
||||
// This ensures that only the binary the daemon is embedded in can retrieve the password.
|
||||
func handleConn(conn net.Conn, sigChan chan os.Signal) {
|
||||
ucred := peercred.Get(conn)
|
||||
|
||||
@@ -84,7 +87,7 @@ func handleConn(conn net.Conn, sigChan chan os.Signal) {
|
||||
// invalid client; close the connection w/o a response,
|
||||
// log the client's path, and kill the daemon
|
||||
conn.Close()
|
||||
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file
|
||||
sigChan <- syscall.SIGTERM
|
||||
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath)
|
||||
sigChan <- syscall.SIGTERM // this zeroizes globalPassword and triggers os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
"github.com/rwinkhart/peercred-mini"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
@@ -24,9 +24,9 @@ const (
|
||||
|
||||
// Start is the entry point for the RPC server responsible for
|
||||
// returning decrypted data to authenticated clients.
|
||||
func Start(passphrase []byte) {
|
||||
// store passphrase to be referenced by DecryptRequest method
|
||||
globalPassphrase = passphrase
|
||||
func Start(password []byte) {
|
||||
// store password to be referenced by DecryptRequest method
|
||||
globalPassword = password
|
||||
|
||||
// register RCWService with the RPC package
|
||||
if err := rpc.Register(&RCWService{}); err != nil {
|
||||
@@ -52,9 +52,9 @@ func Start(passphrase []byte) {
|
||||
defer listener.Close()
|
||||
log.Printf("RPC daemon listening on %s", socketPath)
|
||||
|
||||
// capture sigterms to ensure listener is closed
|
||||
// capture termination signals to ensure listener is closed
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
// create inactivity timer
|
||||
timer := time.NewTimer(time.Duration(Timeout) * time.Second)
|
||||
killTimer := make(chan struct{})
|
||||
@@ -63,11 +63,13 @@ func Start(passphrase []byte) {
|
||||
case <-timer.C:
|
||||
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
|
||||
listener.Close()
|
||||
security.ZeroizeBytes(globalPassword)
|
||||
os.Exit(0)
|
||||
case <-killTimer:
|
||||
return
|
||||
case <-sigChan:
|
||||
listener.Close()
|
||||
security.ZeroizeBytes(globalPassword)
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
@@ -108,8 +110,7 @@ func handleConn(conn net.Conn, sigChan chan os.Signal) {
|
||||
// invalid client; close the connection w/o a response,
|
||||
// log the client's path, and kill the daemon
|
||||
conn.Close()
|
||||
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file
|
||||
sigChan <- syscall.SIGTERM
|
||||
os.Exit(2)
|
||||
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath)
|
||||
sigChan <- os.Interrupt // this zeroizes globalPassword and triggers os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -4,6 +4,8 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
)
|
||||
|
||||
// GetDec requests the RCW daemon to decrypt the given data.
|
||||
@@ -23,14 +25,18 @@ func GetDec(encBytes []byte) []byte {
|
||||
|
||||
// GetEnc requests the RCW daemon to encrypt the given data.
|
||||
// It returns the encrypted data.
|
||||
func GetEnc(decBytes []byte) []byte {
|
||||
func GetEnc(decBytes []byte, zeroizeDecBytes bool) []byte {
|
||||
conn, client := connectToDaemon()
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
// request encBytes from the RPC server
|
||||
var encBytes []byte
|
||||
if err := client.Call("RCWService.EncryptRequest", decBytes, &encBytes); err != nil {
|
||||
err := client.Call("RCWService.EncryptRequest", decBytes, &encBytes)
|
||||
if zeroizeDecBytes {
|
||||
security.ZeroizeBytes(decBytes)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Error calling RCWService.EncryptRequest: %v", err)
|
||||
}
|
||||
return encBytes
|
||||
|
||||
+17
-19
@@ -14,25 +14,25 @@ import (
|
||||
//
|
||||
// Usage:
|
||||
// rcw init <passwd> : Generates the required sanity check file
|
||||
// rcw <passphrase> : Runs the rcw daemon to decrypt data for three minutes
|
||||
// rcw enc <text> : Encrypts the provided text and outputs the ciphertext to ex-cipher.rcw (attempts to use daemon, falls back to user input for passphrase)
|
||||
// rcw dec : Decrypts ex-cipher.rcw and outputs the plaintext to stdout (attempts to use daemon, falls back to user input for passphrase)
|
||||
// rcw <password> : Runs the rcw daemon to decrypt data for three minutes
|
||||
// rcw enc <text> : Encrypts the provided text and outputs the ciphertext to ex-cipher.rcw (attempts to use daemon, falls back to user input for password)
|
||||
// rcw dec : Decrypts ex-cipher.rcw and outputs the plaintext to stdout (attempts to use daemon, falls back to user input for password)
|
||||
|
||||
// Implementation Notes:
|
||||
// There are two main ways to use the RCW library:
|
||||
//
|
||||
// 1. Daemon mode:
|
||||
// The daemon is started with a passphrase and runs in the background.
|
||||
// The daemon is started with a password and runs in the background.
|
||||
// All encryption/decryption occurs in the daemon.
|
||||
// Avoid using the wrapper.Encrypt/Decrypt functions directly.
|
||||
// Instead, cache the passphrase with the daemon and use the daemon to encrypt/decrypt data.
|
||||
// Instead, cache the password with the daemon and use the daemon to encrypt/decrypt data.
|
||||
//
|
||||
// 2. Standalone mode:
|
||||
// The wrapper.Encrypt/Decrypt functions are used directly.
|
||||
// The passphrase is provided directly to the functions.
|
||||
// The password is provided directly to the functions.
|
||||
//
|
||||
// It is up to the client to perform the sanity check before encrypting data.
|
||||
// This means that when using the daemon to cache the passphrase, the client should
|
||||
// This means that when using the daemon to cache the password, the client should
|
||||
// perform the sanity check before activating the daemon.
|
||||
|
||||
// TODO Tests:
|
||||
@@ -66,7 +66,7 @@ func main() {
|
||||
if daemon.IsOpen() {
|
||||
decBytes = daemon.GetDec(encBytes)
|
||||
} else {
|
||||
decBytes, err = wrappers.Decrypt(encBytes, front.InputHidden("Enter RCW passphrase:"))
|
||||
decBytes, err = wrappers.Decrypt(encBytes, front.InputSecret("Enter RCW password:"), true)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
@@ -81,36 +81,34 @@ func main() {
|
||||
fmt.Println("Daemon already running")
|
||||
return
|
||||
}
|
||||
err := wrappers.RunSanityCheck(sanityFile, []byte(os.Args[1]))
|
||||
if err != nil {
|
||||
if err := wrappers.RunSanityCheck(sanityFile, []byte(os.Args[1])); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
daemon.Start([]byte(os.Args[1]))
|
||||
case 3:
|
||||
if os.Args[1] == "init" {
|
||||
switch os.Args[1] {
|
||||
case "init":
|
||||
// create sanity check file
|
||||
// rcw init <passwd>
|
||||
err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2]))
|
||||
if err != nil {
|
||||
if err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2]), true); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return
|
||||
} else if os.Args[1] == "enc" {
|
||||
case "enc":
|
||||
// encrypt data (using daemon if available)
|
||||
// rcw enc <data>
|
||||
decBytes := []byte(os.Args[2])
|
||||
var encBytes []byte
|
||||
if daemon.IsOpen() {
|
||||
encBytes = daemon.GetEnc(decBytes)
|
||||
encBytes = daemon.GetEnc(decBytes, true)
|
||||
} else {
|
||||
passphrase := front.InputHidden("Enter RCW passphrase: ")
|
||||
err := wrappers.RunSanityCheck(sanityFile, passphrase)
|
||||
if err != nil {
|
||||
password := front.InputSecret("Enter RCW password: ")
|
||||
if err := wrappers.RunSanityCheck(sanityFile, password); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
encBytes = wrappers.Encrypt(decBytes, passphrase)
|
||||
encBytes = wrappers.Encrypt(decBytes, password, true, true)
|
||||
}
|
||||
os.WriteFile(outputFile, encBytes, 0600)
|
||||
return
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
module github.com/rwinkhart/rcw
|
||||
|
||||
go 1.25.6
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/rwinkhart/go-boilerplate v0.2.2
|
||||
github.com/rwinkhart/peercred-mini v0.1.2
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/sys v0.40.0
|
||||
github.com/rwinkhart/go-boilerplate v0.3.0
|
||||
github.com/rwinkhart/peercred-mini v0.1.4
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/sys v0.41.0
|
||||
)
|
||||
|
||||
require golang.org/x/term v0.39.0 // indirect
|
||||
require golang.org/x/term v0.40.0 // indirect
|
||||
|
||||
replace golang.org/x/sys => github.com/rwinkhart/sys v0.40.0
|
||||
replace golang.org/x/sys => github.com/rwinkhart/sys v0.41.0
|
||||
|
||||
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio v0.1.0
|
||||
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio v0.1.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
github.com/rwinkhart/go-boilerplate v0.2.2 h1:SVHTAQU+HWFivtUnDBcfrgClJV5ZmHyFS7/uERh7NKU=
|
||||
github.com/rwinkhart/go-boilerplate v0.2.2/go.mod h1:/NVRKGslU20E5xU5YOgXzWxA6aa94BMtv5MtHRTb5Ek=
|
||||
github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg=
|
||||
github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
|
||||
github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo=
|
||||
github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c=
|
||||
github.com/rwinkhart/sys v0.40.0 h1:ZPBbXb+27vLL518sZhhNV89jXK1KbCnFGmYJ2juOQ/c=
|
||||
github.com/rwinkhart/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
github.com/rwinkhart/go-boilerplate v0.3.0 h1:dwlm1mZya1xrATkvm2pHbTIFNb5WRjIc7A3a4Ep2aAM=
|
||||
github.com/rwinkhart/go-boilerplate v0.3.0/go.mod h1:ES13A2r9fnCVfyezwMBgY/RgA4pOIudOUXz3Jk/ikes=
|
||||
github.com/rwinkhart/go-winio v0.1.1 h1:kAJKiqneR7cUR01Wn5/doAAV4kOGTEGPug4oinXc5N4=
|
||||
github.com/rwinkhart/go-winio v0.1.1/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
|
||||
github.com/rwinkhart/peercred-mini v0.1.4 h1:93+phjLknvJadEd2cu/ZPPWdfRSPOwFJzDBEn4ZtWVc=
|
||||
github.com/rwinkhart/peercred-mini v0.1.4/go.mod h1:E8eApo/izzmq4nGGR+kpJ0ZLAXjofnVRMGCeVMPQ/Ik=
|
||||
github.com/rwinkhart/sys v0.41.0 h1:pHB6HphVC132UXYZ6yeOk62abqgl+pyl5ZZnsk4iUKg=
|
||||
github.com/rwinkhart/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -46,6 +48,7 @@ func decryptAES(encBytes, key1 []byte) ([]byte, error) {
|
||||
|
||||
// create AES-256 cipher
|
||||
block, _ := aes.NewCipher(key2)
|
||||
security.ZeroizeBytes(key2)
|
||||
|
||||
// create GCM mode
|
||||
aesGCM, _ := cipher.NewGCM(block)
|
||||
|
||||
@@ -3,6 +3,7 @@ package wrappers
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
@@ -41,6 +42,7 @@ func decryptCha(encBytes, key1 []byte) ([]byte, error) {
|
||||
|
||||
// create ChaCha20-Poly1305 cipher
|
||||
stream, _ := chacha20poly1305.NewX(key2)
|
||||
security.ZeroizeBytes(key2)
|
||||
|
||||
// decrypt the data
|
||||
plaintext, err := stream.Open(nil, nonce, ciphertext, nil)
|
||||
|
||||
+28
-8
@@ -1,15 +1,23 @@
|
||||
package wrappers
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
// Decrypt decrypts the provided byte slice using the provided passphrase.
|
||||
func Decrypt(encBytes, passphrase []byte) ([]byte, error) {
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
)
|
||||
|
||||
// Decrypt decrypts the provided byte slice using the provided password.
|
||||
func Decrypt(encBytes, password []byte, zeroizePassword bool) ([]byte, error) {
|
||||
if len(encBytes) < saltSize1 {
|
||||
return nil, errors.New("High-level decrypt: Encrypted data is too short (invalid Argon2 salt)")
|
||||
return nil, errors.New("high-level decrypt: encrypted data is too short (invalid Argon2 salt)")
|
||||
}
|
||||
salt1 := encBytes[:saltSize1]
|
||||
encBytes = encBytes[saltSize1:]
|
||||
key1 := derivePrimaryKey(passphrase, salt1)
|
||||
key1 := derivePrimaryKey(password, salt1)
|
||||
defer security.ZeroizeBytes(key1)
|
||||
if zeroizePassword {
|
||||
security.ZeroizeBytes(password)
|
||||
}
|
||||
var err error
|
||||
encBytes, err = decryptCha(encBytes, key1)
|
||||
if err != nil {
|
||||
@@ -22,16 +30,28 @@ func Decrypt(encBytes, passphrase []byte) ([]byte, error) {
|
||||
return encBytes, err
|
||||
}
|
||||
|
||||
// Encrypt encrypts the provided byte slice using the provided passphrase.
|
||||
func Encrypt(decBytes, passphrase []byte) []byte {
|
||||
// Encrypt encrypts the provided byte slice using the provided password.
|
||||
func Encrypt(decBytes, password []byte, zeroizeDecBytes, zeroizePassword bool) []byte {
|
||||
if zeroizeDecBytes {
|
||||
defer security.ZeroizeBytes(decBytes)
|
||||
}
|
||||
salt1 := getRandomBytes(saltSize1)
|
||||
defer security.ZeroizeBytes(salt1)
|
||||
salt2AES := getRandomBytes(saltSize2)
|
||||
salt2Cha := getRandomBytes(saltSize2)
|
||||
key1 := derivePrimaryKey(passphrase, salt1)
|
||||
key1 := derivePrimaryKey(password, salt1)
|
||||
if zeroizePassword {
|
||||
security.ZeroizeBytes(password)
|
||||
}
|
||||
key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES))
|
||||
key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha))
|
||||
security.ZeroizeBytes(key1)
|
||||
decBytes = encryptAES(decBytes, key2AES, salt2AES)
|
||||
security.ZeroizeBytes(key2AES)
|
||||
security.ZeroizeBytes(salt2AES)
|
||||
decBytes = encryptCha(decBytes, key2Cha, salt2Cha)
|
||||
security.ZeroizeBytes(key2Cha)
|
||||
security.ZeroizeBytes(salt2Cha)
|
||||
// format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext)
|
||||
return append(append(make([]byte, 0, saltSize1+len(decBytes)), salt1...), decBytes...)
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ const (
|
||||
saltSize2 = 32 // 256 bits, recommended salt size for HKDF
|
||||
)
|
||||
|
||||
// derivePrimaryKey derives an encryption key from a passphrase using Argon2.
|
||||
// derivePrimaryKey derives an encryption key from a password using Argon2.
|
||||
// The resulting key is not meant to be used directly for encryption, but rather as a key to derive other keys.
|
||||
func derivePrimaryKey(passphrase, salt []byte) []byte {
|
||||
return argon2.IDKey(passphrase, salt, argonTime, argonMemory, argonThreads, keyLen)
|
||||
func derivePrimaryKey(password, salt []byte) []byte {
|
||||
return argon2.IDKey(password, salt, argonTime, argonMemory, argonThreads, keyLen)
|
||||
}
|
||||
|
||||
// deriveSecondaryKey derives a secondary key from the primary key using HKDF.
|
||||
// It is meant to be an efficient way to derive multiple keys from a single passphrase.
|
||||
// It is meant to be an efficient way to derive multiple keys from a single password.
|
||||
func deriveSecondaryKey(primaryKey, salt, info []byte) []byte {
|
||||
h := hkdf.New(sha256.New, primaryKey, salt, info)
|
||||
derivedKey := make([]byte, keyLen)
|
||||
|
||||
+12
-9
@@ -1,29 +1,32 @@
|
||||
package wrappers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GenSanityCheck creates an encrypted file containing known plaintext
|
||||
// to later be used for ensuring the user does not encrypt data with
|
||||
// an incorrect passphrase.
|
||||
func GenSanityCheck(path string, passphrase []byte) error {
|
||||
err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), passphrase), 0600)
|
||||
// an incorrect password.
|
||||
func GenSanityCheck(path string, password []byte, zeroizePassword bool) error {
|
||||
err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), password, false, zeroizePassword), 0600)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunSanityCheck should be run before any encryption operation
|
||||
// to ensure the user does not encrypt data with an incorrect passphrase.
|
||||
// to ensure the user does not encrypt data with an incorrect password.
|
||||
// Failure to perform this check could result in data loss.
|
||||
func RunSanityCheck(path string, passphrase []byte) error {
|
||||
func RunSanityCheck(path string, password []byte) error {
|
||||
encBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return errors.New("Failed to read sanity check file (" + path + ")")
|
||||
}
|
||||
decBytes, _ := Decrypt(encBytes, passphrase)
|
||||
if string(decBytes) == "thx4usin'rcw" {
|
||||
return nil
|
||||
decBytes, err := Decrypt(encBytes, password, false)
|
||||
if err == nil {
|
||||
if bytes.Equal(decBytes, []byte("thx4usin'rcw")) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("Sanity check failed (likely due to inconsistent passphrase)")
|
||||
return errors.New("sanity check failed (likely due to inconsistent password)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user