mirror of
https://github.com/rwinkhart/rcw.git
synced 2026-08-27 20:36:30 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8318b2e325 | ||
|
|
ba4a2c8396 | ||
|
|
718a2676c2 | ||
|
|
e79adb82a0 | ||
|
|
e8ef243e87 | ||
|
|
ddd7f893e9 | ||
|
|
f49248930b | ||
|
|
1745b85662 | ||
|
|
c5e259a446 | ||
|
|
4fd72b0042 | ||
|
|
57eefdf8b4 |
@@ -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).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build linux || freebsd
|
||||
//go:build linux || freebsd || solaris || illumos
|
||||
|
||||
package daemon
|
||||
|
||||
|
||||
+13
-8
@@ -2,6 +2,7 @@ package daemon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@@ -11,16 +12,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,16 +29,20 @@ 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)
|
||||
func getFileHash(path string) ([]byte, error) {
|
||||
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()
|
||||
io.Copy(hash, file)
|
||||
return hash.Sum(nil)
|
||||
return hash.Sum(nil), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build solaris || illumos
|
||||
|
||||
package daemon
|
||||
|
||||
const pidPathFile = "path/a.out"
|
||||
@@ -13,22 +13,27 @@ 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 {
|
||||
err := rpc.Register(&RCWService{})
|
||||
if err != nil {
|
||||
log.Fatalf("Error registering RPC service: %v", err)
|
||||
}
|
||||
|
||||
// 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
|
||||
listener, err := net.Listen("unix", socketPath)
|
||||
@@ -38,12 +43,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 +62,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,22 +76,30 @@ 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)
|
||||
|
||||
// check if the RPC call is coming from an identical binary and from the same user
|
||||
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
|
||||
rpc.ServeConn(conn)
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
+25
-12
@@ -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,17 +24,21 @@ 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 {
|
||||
err := rpc.Register(&RCWService{})
|
||||
if err != nil {
|
||||
log.Fatalf("Error registering RPC service: %v", err)
|
||||
}
|
||||
|
||||
// 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
|
||||
pipeConfig := &winio.PipeConfig{
|
||||
@@ -52,9 +56,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 +67,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)
|
||||
}
|
||||
}()
|
||||
@@ -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
|
||||
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)
|
||||
} else {
|
||||
// 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.4
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/rwinkhart/go-boilerplate v0.1.0
|
||||
github.com/rwinkhart/peercred-mini v0.1.2
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/sys v0.38.0
|
||||
github.com/rwinkhart/go-boilerplate v0.3.1
|
||||
github.com/rwinkhart/peercred-mini v0.1.5
|
||||
golang.org/x/crypto v0.52.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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
github.com/rwinkhart/go-boilerplate v0.1.0 h1:EzlVj6R7Bxtl79Nl7R5zRcg6s+Cf2FAqGIzR4giWTQg=
|
||||
github.com/rwinkhart/go-boilerplate v0.1.0/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
|
||||
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.38.0 h1:V1PGKcUutWtD3+VdHvyvfGtPy7PDXzKHNM1IzVq1ysY=
|
||||
github.com/rwinkhart/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
github.com/rwinkhart/go-boilerplate v0.3.1 h1:vkVRuptO2s1yPzzwpvtiiB0c/hPB6yr0p1mzZ2Myb9E=
|
||||
github.com/rwinkhart/go-boilerplate v0.3.1/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.5 h1:9sBR0ascteCJfzfhv7Gq/juqu7OOU+AphiEIG1+LcWA=
|
||||
github.com/rwinkhart/peercred-mini v0.1.5/go.mod h1:C73IeDteQwsKp1+PDdUBj4FC6dTziAJNbQsVDZSWE+M=
|
||||
github.com/rwinkhart/sys v0.45.0 h1:HXZL0SuyToqKkvakaUuRAdH8sPUHelfO6q4PQRpL26Q=
|
||||
github.com/rwinkhart/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
|
||||
@@ -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)
|
||||
|
||||
+32
-7
@@ -1,11 +1,24 @@
|
||||
package wrappers
|
||||
|
||||
// Decrypt decrypts the provided byte slice using the provided passphrase.
|
||||
func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) {
|
||||
var err error = nil
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"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]
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -17,16 +30,28 @@ func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) {
|
||||
return encBytes, err
|
||||
}
|
||||
|
||||
// Encrypt encrypts the provided byte slice using the provided passphrase.
|
||||
func Encrypt(decBytes []byte, 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)
|
||||
|
||||
+13
-10
@@ -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 + ")")
|
||||
return errors.New("unable to read sanity check file (" + path + "): " + err.Error())
|
||||
}
|
||||
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