10 Commits
15 changed files with 185 additions and 101 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).
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build linux || freebsd
//go:build linux || freebsd || solaris || illumos
package daemon
+13 -8
View File
@@ -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
}
+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"
"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
View File
@@ -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
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 -8
View File
@@ -1,17 +1,17 @@
module github.com/rwinkhart/rcw
go 1.25.6
go 1.26.3
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.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.39.0 // indirect
require golang.org/x/term v0.43.0 // indirect
replace golang.org/x/sys => github.com/rwinkhart/sys v0.40.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.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.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=
+3
View File
@@ -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)
+2
View File
@@ -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
View File
@@ -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...)
}
+4 -4
View File
@@ -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
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 + ")")
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)")
}