11 Commits
15 changed files with 189 additions and 100 deletions
+18 -4
View File
@@ -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. It encrypts all data with both AES256-GCM and ChaCha20-Poly1305.
Passphrases are securely cached for three minutes and RPC authentication is used to Passwords 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. ensure that only the binary+user responsible for caching the password can utilize it.
This feature is supported on Linux, FreeBSD, MacOS, and Windows. 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 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. 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. 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 # Usage
For now, please reference [example.go](https://github.com/rwinkhart/randalls-cryptographic-wrappers/blob/main/example.go). 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).
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build linux || freebsd //go:build linux || freebsd || solaris || illumos
package daemon package daemon
+13 -8
View File
@@ -2,6 +2,7 @@ package daemon
import ( import (
"crypto/sha256" "crypto/sha256"
"errors"
"io" "io"
"os" "os"
@@ -11,16 +12,16 @@ import (
var Timeout = 300 // seconds for RPC server timeout; configurable var Timeout = 300 // seconds for RPC server timeout; configurable
var daemonHash []byte var daemonHash []byte
var globalPassphrase []byte var globalPassword []byte
// RCWService provides an RPC method. // RCWService provides an RPC method.
type RCWService struct{} type RCWService struct{}
// DecryptRequest is the RPC method that decrypts the incoming data using // 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 { func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
var err error var err error
*reply, err = wrappers.Decrypt(encBytes, globalPassphrase) *reply, err = wrappers.Decrypt(encBytes, globalPassword, false)
if err != nil { if err != nil {
return err return err
} }
@@ -28,16 +29,20 @@ func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
} }
// EncryptRequest is the RPC method that encrypts the incoming data using // 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 { func (h *RCWService) EncryptRequest(decBytes []byte, reply *[]byte) error {
*reply = wrappers.Encrypt(decBytes, globalPassphrase) *reply = wrappers.Encrypt(decBytes, globalPassword, true, false)
return nil return nil
} }
// getFileHash returns the SHA256 hash of the file at the given path. // getFileHash returns the SHA256 hash of the file at the given path.
func getFileHash(path string) []byte { func getFileHash(path string) ([]byte, error) {
file, _ := os.Open(path) file, err := os.Open(path)
if err != nil {
return nil, errors.New("unable to read path (" + path + ") for hashing: " + err.Error())
}
defer file.Close()
hash := sha256.New() hash := sha256.New()
io.Copy(hash, file) io.Copy(hash, file)
return hash.Sum(nil) return hash.Sum(nil), nil
} }
+5
View File
@@ -0,0 +1,5 @@
//go:build solaris || illumos
package daemon
const pidPathFile = "path/a.out"
+28 -13
View File
@@ -13,22 +13,27 @@ import (
"syscall" "syscall"
"time" "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 // Start is the entry point for the RPC server responsible for
// returning decrypted data to authenticated clients. // returning decrypted data to authenticated clients.
func Start(passphrase []byte) { func Start(password []byte) {
// store passphrase to be referenced by DecryptRequest method // store password to be referenced by DecryptRequest method
globalPassphrase = passphrase globalPassword = password
// register RCWService with the RPC package // register RCWService with the RPC package
if err := rpc.Register(&RCWService{}); err != nil { err := rpc.Register(&RCWService{})
if err != nil {
log.Fatalf("Error registering RPC service: %v", err) log.Fatalf("Error registering RPC service: %v", err)
} }
// store the hash of the daemon binary // store the hash of the daemon binary
daemonHash = getFileHash(binPath) daemonHash, err = getFileHash(binPath)
if err != nil {
log.Fatalf("Error hashing daemon binary: %v", err)
}
// listen on the Unix domain socket // listen on the Unix domain socket
listener, err := net.Listen("unix", socketPath) listener, err := net.Listen("unix", socketPath)
@@ -38,12 +43,13 @@ func Start(passphrase []byte) {
defer listener.Close() defer listener.Close()
log.Printf("RPC daemon listening on unix://%s", socketPath) 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) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() { go func() {
<-sigChan <-sigChan
listener.Close() listener.Close()
security.ZeroizeBytes(globalPassword)
os.Exit(0) os.Exit(0)
}() }()
@@ -56,6 +62,7 @@ func Start(passphrase []byte) {
if err.(net.Error).Timeout() { if err.(net.Error).Timeout() {
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...") log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
listener.Close() listener.Close()
security.ZeroizeBytes(globalPassword)
os.Exit(0) os.Exit(0)
} }
log.Printf("Accept error: %v", err) log.Printf("Accept error: %v", err)
@@ -69,22 +76,30 @@ func Start(passphrase []byte) {
// handleConn verifies the identity of the client. // handleConn verifies the identity of the client.
// It uses the file descriptor of the connection to get the PID 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. // 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. // 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) { func handleConn(conn net.Conn, sigChan chan os.Signal) {
ucred := peercred.Get(conn) ucred := peercred.Get(conn)
// check if the RPC call is coming from an identical binary and from the same user // check if the RPC call is coming from an identical binary and from the same user
callingBinPath := pidToPath(ucred.PID) callingBinPath := pidToPath(ucred.PID)
if ucred.UID == strconv.Itoa(os.Getuid()) && bytes.Equal(getFileHash(callingBinPath), daemonHash) { callingBinHash, err := getFileHash(callingBinPath)
if err != nil {
// calling binary hash failure
conn.Close()
log.Printf("Failed to hash calling binary: PID(%d), UID(%s), Path(%s) - %v", ucred.PID, ucred.UID, callingBinPath, err)
sigChan <- syscall.SIGTERM // this zeroizes globalPassword and triggers os.Exit(0)
return // explicitly return to avoid race
}
if ucred.UID == strconv.Itoa(os.Getuid()) && bytes.Equal(callingBinHash, daemonHash) {
// valid client; hand off the connection to the RPC server // valid client; hand off the connection to the RPC server
rpc.ServeConn(conn) rpc.ServeConn(conn)
} else { } else {
// invalid client; close the connection w/o a response, // invalid client; close the connection w/o a response,
// log the client's path, and kill the daemon // log the client's path, and kill the daemon
conn.Close() conn.Close()
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath)
sigChan <- syscall.SIGTERM sigChan <- syscall.SIGTERM // this zeroizes globalPassword and triggers os.Exit(0)
} }
} }
+25 -12
View File
@@ -10,10 +10,10 @@ import (
"os" "os"
"os/signal" "os/signal"
"strconv" "strconv"
"syscall"
"time" "time"
"github.com/Microsoft/go-winio" "github.com/Microsoft/go-winio"
"github.com/rwinkhart/go-boilerplate/security"
"github.com/rwinkhart/peercred-mini" "github.com/rwinkhart/peercred-mini"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -24,17 +24,21 @@ const (
// Start is the entry point for the RPC server responsible for // Start is the entry point for the RPC server responsible for
// returning decrypted data to authenticated clients. // returning decrypted data to authenticated clients.
func Start(passphrase []byte) { func Start(password []byte) {
// store passphrase to be referenced by DecryptRequest method // store password to be referenced by DecryptRequest method
globalPassphrase = passphrase globalPassword = password
// register RCWService with the RPC package // register RCWService with the RPC package
if err := rpc.Register(&RCWService{}); err != nil { err := rpc.Register(&RCWService{})
if err != nil {
log.Fatalf("Error registering RPC service: %v", err) log.Fatalf("Error registering RPC service: %v", err)
} }
// store the hash of the daemon binary // store the hash of the daemon binary
daemonHash = getFileHash(binPath) daemonHash, err = getFileHash(binPath)
if err != nil {
log.Fatalf("Error hashing daemon binary: %v", err)
}
// configure the named pipe // configure the named pipe
pipeConfig := &winio.PipeConfig{ pipeConfig := &winio.PipeConfig{
@@ -52,9 +56,9 @@ func Start(passphrase []byte) {
defer listener.Close() defer listener.Close()
log.Printf("RPC daemon listening on %s", socketPath) 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) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) signal.Notify(sigChan, os.Interrupt)
// create inactivity timer // create inactivity timer
timer := time.NewTimer(time.Duration(Timeout) * time.Second) timer := time.NewTimer(time.Duration(Timeout) * time.Second)
killTimer := make(chan struct{}) killTimer := make(chan struct{})
@@ -63,11 +67,13 @@ func Start(passphrase []byte) {
case <-timer.C: case <-timer.C:
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...") log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
listener.Close() listener.Close()
security.ZeroizeBytes(globalPassword)
os.Exit(0) os.Exit(0)
case <-killTimer: case <-killTimer:
return return
case <-sigChan: case <-sigChan:
listener.Close() listener.Close()
security.ZeroizeBytes(globalPassword)
os.Exit(0) os.Exit(0)
} }
}() }()
@@ -102,14 +108,21 @@ func handleConn(conn net.Conn, sigChan chan os.Signal) {
// check if the RPC call is coming from an identical binary and from the same user // check if the RPC call is coming from an identical binary and from the same user
callingBinPath := pidToPath(uint32(ucred.PID)) callingBinPath := pidToPath(uint32(ucred.PID))
if ucred.UID == user.User.Sid.String() && bytes.Equal(getFileHash(callingBinPath), daemonHash) { callingBinHash, err := getFileHash(callingBinPath)
if err != nil {
// calling binary hash failure
conn.Close()
log.Printf("Failed to hash calling binary: PID(%d), UID(%s), Path(%s) - %v", ucred.PID, ucred.UID, callingBinPath, err)
sigChan <- os.Interrupt // this zeroizes globalPassword and triggers os.Exit(0)
return // explicitly return to avoid race
}
if ucred.UID == user.User.Sid.String() && bytes.Equal(callingBinHash, daemonHash) {
rpc.ServeConn(conn) rpc.ServeConn(conn)
} else { } else {
// invalid client; close the connection w/o a response, // invalid client; close the connection w/o a response,
// log the client's path, and kill the daemon // log the client's path, and kill the daemon
conn.Close() conn.Close()
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath)
sigChan <- syscall.SIGTERM sigChan <- os.Interrupt // this zeroizes globalPassword and triggers os.Exit(0)
os.Exit(2)
} }
} }
+8 -2
View File
@@ -4,6 +4,8 @@ import (
"log" "log"
"net" "net"
"net/rpc" "net/rpc"
"github.com/rwinkhart/go-boilerplate/security"
) )
// GetDec requests the RCW daemon to decrypt the given data. // 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. // GetEnc requests the RCW daemon to encrypt the given data.
// It returns the encrypted data. // It returns the encrypted data.
func GetEnc(decBytes []byte) []byte { func GetEnc(decBytes []byte, zeroizeDecBytes bool) []byte {
conn, client := connectToDaemon() conn, client := connectToDaemon()
defer conn.Close() defer conn.Close()
defer client.Close() defer client.Close()
// request encBytes from the RPC server // request encBytes from the RPC server
var encBytes []byte 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) log.Fatalf("Error calling RCWService.EncryptRequest: %v", err)
} }
return encBytes return encBytes
+17 -19
View File
@@ -14,25 +14,25 @@ import (
// //
// Usage: // Usage:
// rcw init <passwd> : Generates the required sanity check file // rcw init <passwd> : Generates the required sanity check file
// rcw <passphrase> : Runs the rcw daemon to decrypt data for three minutes // 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 passphrase) // 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 passphrase) // 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: // Implementation Notes:
// There are two main ways to use the RCW library: // There are two main ways to use the RCW library:
// //
// 1. Daemon mode: // 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. // All encryption/decryption occurs in the daemon.
// Avoid using the wrapper.Encrypt/Decrypt functions directly. // 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: // 2. Standalone mode:
// The wrapper.Encrypt/Decrypt functions are used directly. // 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. // 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. // perform the sanity check before activating the daemon.
// TODO Tests: // TODO Tests:
@@ -66,7 +66,7 @@ func main() {
if daemon.IsOpen() { if daemon.IsOpen() {
decBytes = daemon.GetDec(encBytes) decBytes = daemon.GetDec(encBytes)
} else { } 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 { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
@@ -81,36 +81,34 @@ func main() {
fmt.Println("Daemon already running") fmt.Println("Daemon already running")
return return
} }
err := wrappers.RunSanityCheck(sanityFile, []byte(os.Args[1])) if err := wrappers.RunSanityCheck(sanityFile, []byte(os.Args[1])); err != nil {
if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
daemon.Start([]byte(os.Args[1])) daemon.Start([]byte(os.Args[1]))
case 3: case 3:
if os.Args[1] == "init" { switch os.Args[1] {
case "init":
// create sanity check file // create sanity check file
// rcw init <passwd> // rcw init <passwd>
err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2])) if err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2]), true); err != nil {
if err != nil {
fmt.Println(err) fmt.Println(err)
} }
return return
} else if os.Args[1] == "enc" { case "enc":
// encrypt data (using daemon if available) // encrypt data (using daemon if available)
// rcw enc <data> // rcw enc <data>
decBytes := []byte(os.Args[2]) decBytes := []byte(os.Args[2])
var encBytes []byte var encBytes []byte
if daemon.IsOpen() { if daemon.IsOpen() {
encBytes = daemon.GetEnc(decBytes) encBytes = daemon.GetEnc(decBytes, true)
} else { } else {
passphrase := front.InputHidden("Enter RCW passphrase: ") password := front.InputSecret("Enter RCW password: ")
err := wrappers.RunSanityCheck(sanityFile, passphrase) if err := wrappers.RunSanityCheck(sanityFile, password); err != nil {
if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
encBytes = wrappers.Encrypt(decBytes, passphrase) encBytes = wrappers.Encrypt(decBytes, password, true, true)
} }
os.WriteFile(outputFile, encBytes, 0600) os.WriteFile(outputFile, encBytes, 0600)
return return
+8 -8
View File
@@ -1,17 +1,17 @@
module github.com/rwinkhart/rcw module github.com/rwinkhart/rcw
go 1.25.4 go 1.26.3
require ( require (
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/rwinkhart/go-boilerplate v0.1.0 github.com/rwinkhart/go-boilerplate v0.3.1
github.com/rwinkhart/peercred-mini v0.1.2 github.com/rwinkhart/peercred-mini v0.1.5
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.52.0
golang.org/x/sys v0.38.0 golang.org/x/sys v0.45.0
) )
require golang.org/x/term v0.37.0 // indirect require golang.org/x/term v0.43.0 // indirect
replace golang.org/x/sys => github.com/rwinkhart/sys v0.38.0 replace golang.org/x/sys => github.com/rwinkhart/sys v0.45.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
+12 -12
View File
@@ -1,12 +1,12 @@
github.com/rwinkhart/go-boilerplate v0.1.0 h1:EzlVj6R7Bxtl79Nl7R5zRcg6s+Cf2FAqGIzR4giWTQg= github.com/rwinkhart/go-boilerplate v0.3.1 h1:vkVRuptO2s1yPzzwpvtiiB0c/hPB6yr0p1mzZ2Myb9E=
github.com/rwinkhart/go-boilerplate v0.1.0/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY= github.com/rwinkhart/go-boilerplate v0.3.1/go.mod h1:ES13A2r9fnCVfyezwMBgY/RgA4pOIudOUXz3Jk/ikes=
github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg= github.com/rwinkhart/go-winio v0.1.1 h1:kAJKiqneR7cUR01Wn5/doAAV4kOGTEGPug4oinXc5N4=
github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/rwinkhart/go-winio v0.1.1/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.5 h1:9sBR0ascteCJfzfhv7Gq/juqu7OOU+AphiEIG1+LcWA=
github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c= github.com/rwinkhart/peercred-mini v0.1.5/go.mod h1:C73IeDteQwsKp1+PDdUBj4FC6dTziAJNbQsVDZSWE+M=
github.com/rwinkhart/sys v0.38.0 h1:V1PGKcUutWtD3+VdHvyvfGtPy7PDXzKHNM1IzVq1ysY= github.com/rwinkhart/sys v0.45.0 h1:HXZL0SuyToqKkvakaUuRAdH8sPUHelfO6q4PQRpL26Q=
github.com/rwinkhart/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= github.com/rwinkhart/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
+3
View File
@@ -4,6 +4,8 @@ import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"errors" "errors"
"github.com/rwinkhart/go-boilerplate/security"
) )
const ( const (
@@ -46,6 +48,7 @@ func decryptAES(encBytes, key1 []byte) ([]byte, error) {
// create AES-256 cipher // create AES-256 cipher
block, _ := aes.NewCipher(key2) block, _ := aes.NewCipher(key2)
security.ZeroizeBytes(key2)
// create GCM mode // create GCM mode
aesGCM, _ := cipher.NewGCM(block) aesGCM, _ := cipher.NewGCM(block)
+2
View File
@@ -3,6 +3,7 @@ package wrappers
import ( import (
"errors" "errors"
"github.com/rwinkhart/go-boilerplate/security"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
) )
@@ -41,6 +42,7 @@ func decryptCha(encBytes, key1 []byte) ([]byte, error) {
// create ChaCha20-Poly1305 cipher // create ChaCha20-Poly1305 cipher
stream, _ := chacha20poly1305.NewX(key2) stream, _ := chacha20poly1305.NewX(key2)
security.ZeroizeBytes(key2)
// decrypt the data // decrypt the data
plaintext, err := stream.Open(nil, nonce, ciphertext, nil) plaintext, err := stream.Open(nil, nonce, ciphertext, nil)
+32 -7
View File
@@ -1,11 +1,24 @@
package wrappers package wrappers
// Decrypt decrypts the provided byte slice using the provided passphrase. import (
func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) { "errors"
var err error = nil
"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)")
}
salt1 := encBytes[:saltSize1] salt1 := encBytes[:saltSize1]
encBytes = 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) encBytes, err = decryptCha(encBytes, key1)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -17,16 +30,28 @@ func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) {
return encBytes, err return encBytes, err
} }
// Encrypt encrypts the provided byte slice using the provided passphrase. // Encrypt encrypts the provided byte slice using the provided password.
func Encrypt(decBytes []byte, passphrase []byte) []byte { func Encrypt(decBytes, password []byte, zeroizeDecBytes, zeroizePassword bool) []byte {
if zeroizeDecBytes {
defer security.ZeroizeBytes(decBytes)
}
salt1 := getRandomBytes(saltSize1) salt1 := getRandomBytes(saltSize1)
defer security.ZeroizeBytes(salt1)
salt2AES := getRandomBytes(saltSize2) salt2AES := getRandomBytes(saltSize2)
salt2Cha := getRandomBytes(saltSize2) salt2Cha := getRandomBytes(saltSize2)
key1 := derivePrimaryKey(passphrase, salt1) key1 := derivePrimaryKey(password, salt1)
if zeroizePassword {
security.ZeroizeBytes(password)
}
key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES)) key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES))
key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha)) key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha))
security.ZeroizeBytes(key1)
decBytes = encryptAES(decBytes, key2AES, salt2AES) decBytes = encryptAES(decBytes, key2AES, salt2AES)
security.ZeroizeBytes(key2AES)
security.ZeroizeBytes(salt2AES)
decBytes = encryptCha(decBytes, key2Cha, salt2Cha) decBytes = encryptCha(decBytes, key2Cha, salt2Cha)
security.ZeroizeBytes(key2Cha)
security.ZeroizeBytes(salt2Cha)
// format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext) // format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext)
return append(append(make([]byte, 0, saltSize1+len(decBytes)), salt1...), decBytes...) return append(append(make([]byte, 0, saltSize1+len(decBytes)), salt1...), decBytes...)
} }
+4 -4
View File
@@ -21,14 +21,14 @@ const (
saltSize2 = 32 // 256 bits, recommended salt size for HKDF 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. // 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 { func derivePrimaryKey(password, salt []byte) []byte {
return argon2.IDKey(passphrase, salt, argonTime, argonMemory, argonThreads, keyLen) return argon2.IDKey(password, salt, argonTime, argonMemory, argonThreads, keyLen)
} }
// deriveSecondaryKey derives a secondary key from the primary key using HKDF. // 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 { func deriveSecondaryKey(primaryKey, salt, info []byte) []byte {
h := hkdf.New(sha256.New, primaryKey, salt, info) h := hkdf.New(sha256.New, primaryKey, salt, info)
derivedKey := make([]byte, keyLen) derivedKey := make([]byte, keyLen)
+13 -10
View File
@@ -1,29 +1,32 @@
package wrappers package wrappers
import ( import (
"bytes"
"errors" "errors"
"os" "os"
) )
// GenSanityCheck creates an encrypted file containing known plaintext // GenSanityCheck creates an encrypted file containing known plaintext
// to later be used for ensuring the user does not encrypt data with // to later be used for ensuring the user does not encrypt data with
// an incorrect passphrase. // an incorrect password.
func GenSanityCheck(path string, passphrase []byte) error { func GenSanityCheck(path string, password []byte, zeroizePassword bool) error {
err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), passphrase), 0600) err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), password, false, zeroizePassword), 0600)
return err return err
} }
// RunSanityCheck should be run before any encryption operation // 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. // 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) encBytes, err := os.ReadFile(path)
if err != nil { if err != nil {
return errors.New("Failed to read sanity check file (" + path + ")") return errors.New("unable to read sanity check file (" + path + "): " + err.Error())
} }
decBytes, _ := Decrypt(encBytes, passphrase) decBytes, err := Decrypt(encBytes, password, false)
if string(decBytes) == "thx4usin'rcw" { if err == nil {
return 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)")
} }