18 Commits
Author SHA1 Message Date
RandyTheSilly e79adb82a0 Bump go-boilerplate and peercred-mini 2026-02-13 17:34:02 -05:00
RandyTheSilly e8ef243e87 Make input zeroization optional 2026-02-10 23:34:30 -05:00
RandyTheSilly ddd7f893e9 Fix password being zeroized in RunSanityCheck() 2026-02-10 21:54:34 -05:00
RandyTheSilly f49248930b Rename all occurrences of "passphrase" to "password" 2026-02-10 21:14:49 -05:00
RandyTheSilly 1745b85662 Use defer to close binary hash subject 2026-02-10 00:36:32 -05:00
RandyTheSilly c5e259a446 Significant hardening (always work on bytes; zeroize everything); address JetBrains warnings 2026-02-10 00:33:56 -05:00
RandyTheSilly 4fd72b0042 Tidy signal capturing; bump dependencies 2026-02-09 22:59:24 -05:00
RandyTheSilly 57eefdf8b4 Return error from wrappers.Decrypt() if Argon2 salt is not present 2026-01-25 15:57:26 -05:00
RandyTheSilly 2520e2459e Bump golang.org/x/crypto to address CVE-2025-58181 and CVE-2025-47914 2025-11-23 03:45:33 -05:00
RandyTheSilly 1e59bf01a5 Bump dependencies; switch to tagged go-winio fork 2025-11-09 15:40:18 -05:00
RandyTheSilly b96fe34a54 Bump Go to v1.24.6; bump dependencies 2025-08-17 13:10:23 -04:00
RandyTheSilly 400edc7c63 Bump dependencies 2025-06-19 18:07:54 -04:00
RandyTheSilly 4210673b84 Make server timeout configurable at compile time 2025-05-19 20:22:46 -04:00
RandyTheSilly 8907fdc36e Optimize salt+nonce appends 2025-05-19 20:10:03 -04:00
RandyTheSilly 0e7a796bb1 Derive HKDF keys (per algo) from primary key: roughly doubles time/security efficiency 2025-05-19 19:59:57 -04:00
RandyTheSilly 13ae080af2 Define socketPath for interactive clients 2025-05-12 21:57:37 -04:00
RandyTheSilly 4bcff52758 Fix build conflict 2025-05-11 16:41:03 -04:00
RandyTheSilly 4ddd3cf32c Set socketPath for Termux 2025-05-11 16:38:41 -04:00
17 changed files with 238 additions and 165 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.
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).
+5
View File
@@ -0,0 +1,5 @@
//go:build interactive
package daemon
var socketPath string // interactive clients are not expected to daemonize
+7
View File
@@ -0,0 +1,7 @@
//go:build android && termux && !interactive
package daemon
import "path/filepath"
var socketPath = "/data/data/com.termux/files/usr/tmp/" + filepath.Base(binPath) + "-rcwd.sock" // store UNIX socket path
@@ -1,9 +1,7 @@
//go:build !windows
//go:build !windows && !android && !termux && !interactive
package daemon
import (
"path/filepath"
)
import "path/filepath"
var socketPath = "/tmp/" + filepath.Base(binPath) + "-rcwd.sock" // store UNIX socket path
@@ -1,4 +1,4 @@
//go:build windows
//go:build windows && !interactive
package daemon
+8 -5
View File
@@ -8,17 +8,19 @@ import (
"github.com/rwinkhart/rcw/wrappers"
)
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
}
@@ -26,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)
+16 -13
View File
@@ -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,24 +39,26 @@ 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)
}()
// accept connections (timeout after 3 minutes of inactivity)
// accept connections until Timeout takes effect
for {
listener.(*net.UnixListener).SetDeadline(time.Now().Add(3 * time.Minute))
listener.(*net.UnixListener).SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
conn, err := listener.Accept()
if err != nil {
if err.(net.Error).Timeout() {
log.Println("Three minutes have passed without any connections. Exiting...")
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)
}
}
+14 -12
View File
@@ -9,10 +9,11 @@ import (
"net/rpc"
"os"
"os/signal"
"syscall"
"strconv"
"time"
"github.com/Microsoft/go-winio"
"github.com/rwinkhart/go-boilerplate/security"
"github.com/rwinkhart/peercred-mini"
"golang.org/x/sys/windows"
)
@@ -23,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 {
@@ -51,22 +52,24 @@ 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)
// create 3-minute inactivity timer
timer := time.NewTimer(3 * time.Minute)
signal.Notify(sigChan, os.Interrupt)
// create inactivity timer
timer := time.NewTimer(time.Duration(Timeout) * time.Second)
killTimer := make(chan struct{})
go func() {
select {
case <-timer.C:
log.Println("Three minutes have passed without any connections. Exiting...")
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)
}
}()
@@ -107,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
View File
@@ -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
View File
@@ -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
+8 -9
View File
@@ -1,18 +1,17 @@
module github.com/rwinkhart/rcw
go 1.24.3
require github.com/rwinkhart/peercred-mini v0.1.0
go 1.25.7
require (
github.com/Microsoft/go-winio v0.6.2
github.com/rwinkhart/go-boilerplate v0.0.0-20250509173525-20670ec7bb9c
golang.org/x/crypto v0.38.0
golang.org/x/sys v0.33.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.32.0 // indirect
require golang.org/x/term v0.40.0 // indirect
replace golang.org/x/sys => github.com/rwinkhart/sys-freebsd-13-xucred v0.33.0
replace golang.org/x/sys => github.com/rwinkhart/sys v0.41.0
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410
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.0.0-20250509173525-20670ec7bb9c h1:RIMnYf1MwsvmAr9E0/cpn7rTh8BWdfsQ2r31ITKJp2A=
github.com/rwinkhart/go-boilerplate v0.0.0-20250509173525-20670ec7bb9c/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE=
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/rwinkhart/peercred-mini v0.1.0 h1:TiS6u8cEWzW55S9X4iVpU72Iuy/NG6rMUJMtUEVEFLw=
github.com/rwinkhart/peercred-mini v0.1.0/go.mod h1:+x4Mxc2veE+YePSOpcUasjimh7n799c1KraLuQwHR20=
github.com/rwinkhart/sys-freebsd-13-xucred v0.33.0 h1:W4J4cS0yLMLKuYePFFL2OR8MAYLoHkErJvq9FBBqK70=
github.com/rwinkhart/sys-freebsd-13-xucred v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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=
+18 -28
View File
@@ -3,62 +3,52 @@ package wrappers
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
"github.com/rwinkhart/go-boilerplate/security"
)
const (
nonceSizeAES = 12 // GCM standard nonce size is 12 bytes
hkdfInfoAES = "AES256-GCM"
hkdfInfoCha = "ChaCha20-Poly1305"
)
// EncryptAES encrypts data using AES-256-GCM.
func encryptAES(decBytes []byte, passphrase []byte) []byte {
// generate a random salt
salt := make([]byte, saltSize)
io.ReadFull(rand.Reader, salt)
// derive key from passphrase using the salt
key := deriveKey(passphrase, salt)
func encryptAES(decBytes, key2, salt2 []byte) []byte {
// create AES-256 cipher
block, _ := aes.NewCipher(key)
block, _ := aes.NewCipher(key2)
// create GCM mode
aesGCM, _ := cipher.NewGCM(block)
// generate a random nonce
nonce := make([]byte, nonceSizeAES)
io.ReadFull(rand.Reader, nonce)
nonce := getRandomBytes(nonceSizeAES)
// encrypt the data
ciphertext := aesGCM.Seal(nil, nonce, decBytes, nil)
// format: salt + nonce + ciphertext
result := make([]byte, 0, saltSize+nonceSizeAES+len(ciphertext))
result = append(result, salt...)
result = append(result, nonce...)
result = append(result, ciphertext...)
return result
// format: salt2 + nonce + ciphertext
return append(append(append(make([]byte, 0, saltSize2+nonceSizeAES+len(ciphertext)), salt2...), nonce...), ciphertext...)
}
// DecryptAES decrypts data using AES256-GCM.
func decryptAES(encBytes []byte, passphrase []byte) ([]byte, error) {
if len(encBytes) < saltSize+nonceSizeAES {
func decryptAES(encBytes, key1 []byte) ([]byte, error) {
if len(encBytes) < saltSize2+nonceSizeAES {
return nil, errors.New("AES256-GCM: Encrypted data is too short")
}
// extract salt, nonce, and ciphertext
salt := encBytes[:saltSize]
nonce := encBytes[saltSize : saltSize+nonceSizeAES]
ciphertext := encBytes[saltSize+nonceSizeAES:]
salt2 := encBytes[:saltSize2]
nonce := encBytes[saltSize2 : saltSize2+nonceSizeAES]
ciphertext := encBytes[saltSize2+nonceSizeAES:]
// derive key from passphrase using the salt
key := deriveKey(passphrase, salt)
// derive secondary key from primary key using the salt
key2 := deriveSecondaryKey(key1, salt2, []byte(hkdfInfoAES))
// create AES-256 cipher
block, _ := aes.NewCipher(key)
block, _ := aes.NewCipher(key2)
security.ZeroizeBytes(key2)
// create GCM mode
aesGCM, _ := cipher.NewGCM(block)
+16 -29
View File
@@ -1,10 +1,9 @@
package wrappers
import (
"crypto/rand"
"errors"
"io"
"github.com/rwinkhart/go-boilerplate/security"
"golang.org/x/crypto/chacha20poly1305"
)
@@ -13,49 +12,37 @@ const (
)
// EncryptCha encrypts data using ChaCha20-Poly1305.
func encryptCha(data []byte, passphrase []byte) []byte {
// generate a random salt
salt := make([]byte, saltSize)
io.ReadFull(rand.Reader, salt)
// derive key from passphrase using the salt
key := deriveKey(passphrase, salt)
func encryptCha(decBytes, key2, salt2 []byte) []byte {
// create ChaCha20-Poly1305 cipher
stream, _ := chacha20poly1305.NewX(key)
stream, _ := chacha20poly1305.NewX(key2)
// generate a random nonce
nonce := make([]byte, nonceSizeCha)
io.ReadFull(rand.Reader, nonce)
nonce := getRandomBytes(nonceSizeCha)
// encrypt the data
ciphertext := stream.Seal(nil, nonce, data, nil)
ciphertext := stream.Seal(nil, nonce, decBytes, nil)
// format: salt + nonce + ciphertext
result := make([]byte, 0, saltSize+nonceSizeCha+len(ciphertext))
result = append(result, salt...)
result = append(result, nonce...)
result = append(result, ciphertext...)
return result
// format: salt2 + nonce + ciphertext
return append(append(append(make([]byte, 0, saltSize2+nonceSizeCha+len(ciphertext)), salt2...), nonce...), ciphertext...)
}
// DecryptCha decrypts data using ChaCha20-Poly1305.
func decryptCha(encryptedData []byte, passphrase []byte) ([]byte, error) {
if len(encryptedData) < saltSize+nonceSizeCha {
func decryptCha(encBytes, key1 []byte) ([]byte, error) {
if len(encBytes) < saltSize2+nonceSizeCha {
return nil, errors.New("ChaCha20-Poly1305: Encrypted data is too short")
}
// extract salt, nonce, and ciphertext
salt := encryptedData[:saltSize]
nonce := encryptedData[saltSize : saltSize+nonceSizeCha]
ciphertext := encryptedData[saltSize+nonceSizeCha:]
salt2 := encBytes[:saltSize2]
nonce := encBytes[saltSize2 : saltSize2+nonceSizeCha]
ciphertext := encBytes[saltSize2+nonceSizeCha:]
// derive key from passphrase using the salt
key := deriveKey(passphrase, salt)
// derive secondary key from primary key using the salt
key2 := deriveSecondaryKey(key1, salt2, []byte(hkdfInfoCha))
// create ChaCha20-Poly1305 cipher
stream, _ := chacha20poly1305.NewX(key)
stream, _ := chacha20poly1305.NewX(key2)
security.ZeroizeBytes(key2)
// decrypt the data
plaintext, err := stream.Open(nil, nonce, ciphertext, nil)
+45 -10
View File
@@ -1,22 +1,57 @@
package wrappers
// Decrypt decrypts the provided byte slice using the provided passphrase.
func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) {
var err error = nil
encBytes, err = decryptCha(encBytes, passphrase)
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(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
}
encBytes, err = decryptAES(encBytes, passphrase)
encBytes, err = decryptAES(encBytes, key1)
if err != nil {
return nil, err
}
return encBytes, err
}
// Encrypt encrypts the provided byte slice using the provided passphrase.
func Encrypt(decBytes []byte, passphrase []byte) []byte {
decBytes = encryptAES(decBytes, passphrase)
decBytes = encryptCha(decBytes, passphrase)
return decBytes
// 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(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...)
}
+31 -8
View File
@@ -1,21 +1,44 @@
package wrappers
import (
"crypto/rand"
"crypto/sha256"
"io"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/hkdf"
)
const (
// parameters for Argon2
argonTime = 8 // set to pass 1-second test in dev environment
argonMemory = 384 * 1024 // 384 MB (target running comfortably on a Pi Zero/512 MB RAM)
argonThreads = 32 // must use a static thread count for support across multiple devices
argonTime = 5 // pass 1-second test on dev environment
argonMemory = 1024 * 1024 // 1 GB
argonThreads = 32 // 32 threads offers the best balance between utilization on high-end devices and performance on low-end devices
// general constants
keyLen = 32 // 256 bits, key length for both algorithms
saltSize = 16 // 128 bits, recommended salt size for both algorithms
keyLen = 32 // 256 bits, key length for both algorithms
saltSize1 = 16 // 128 bits, recommended salt size for AES256/ChaCha20/Argon2
saltSize2 = 32 // 256 bits, recommended salt size for HKDF
)
// deriveKey derives an encryption key from a passphrase using Argon2.
func deriveKey(passphrase []byte, salt []byte) []byte {
return argon2.IDKey(passphrase, salt, argonTime, argonMemory, argonThreads, keyLen)
// 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(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 password.
func deriveSecondaryKey(primaryKey, salt, info []byte) []byte {
h := hkdf.New(sha256.New, primaryKey, salt, info)
derivedKey := make([]byte, keyLen)
io.ReadFull(h, derivedKey)
return derivedKey
}
// getRandomBytes returns a random salt/nonce of the specified size.
func getRandomBytes(size uint8) []byte {
salt := make([]byte, size)
io.ReadFull(rand.Reader, salt)
return salt
}
+12 -9
View File
@@ -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)")
}