From bfc6d97773d3a5ac702b0d9d12c01998787897fd Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Sat, 7 Feb 2026 23:07:26 -0500 Subject: [PATCH] Significant enhancements to secure data handling (especially on iOS) --- 0.go | 9 ++++++++- README.md | 58 ------------------------------------------------------ age.go | 4 +++- clip.go | 4 +++- core.go | 13 ++++++------ crypt.go | 6 ++++-- go.mod | 4 ++-- go.sum | 8 ++++---- privkey.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 privkey.go diff --git a/0.go b/0.go index e498ff8..97e2d72 100644 --- a/0.go +++ b/0.go @@ -43,12 +43,13 @@ func getCPascalStringArrayFromStringSlice(goSlice []string) C.PascalStringArray } // getStringSliceFromCPascalStringArray converts a C.PascalStringArray to a go slice. -func getStringSliceFromCPascalStringArray(pascalStringArray C.PascalStringArray) []string { +func getStringSliceFromCPascalStringArrayAndFree(pascalStringArray C.PascalStringArray) []string { var goSlice []string cArr := (*[1 << 30]C.PascalString)(unsafe.Pointer(pascalStringArray.data))[:pascalStringArray.len:pascalStringArray.len] for i := 0; i < int(pascalStringArray.len); i++ { goSlice = append(goSlice, C.GoStringN(cArr[i].data, cArr[i].len)) } + FreePascalArray(pascalStringArray) return goSlice } @@ -96,4 +97,10 @@ func safeBoolDeref(b *bool) bool { return *b } +// securePtrOverwrite securely overwrites the memory at the input pointer and then frees it. +func securePtrOverwriteAndFree(input unsafe.Pointer, length C.int) { + C.explicit_bzero(input, C.size_t(length)) + C.free(input) +} + func main() {} diff --git a/README.md b/README.md index 50d5b61..451e7b1 100644 --- a/README.md +++ b/README.md @@ -65,61 +65,3 @@ int main() { FreeArray(result.r1); } ``` - -# Progress -- [X] age - - [X] AllPasswordEntries(forceReage bool, rcwPassword []byte) error - - [X] Entry(vanityPath string, timestamp int64) error - - [X] TranslateAgeTimestamp(timestamp *int64) uint8 -- [X] clip - - [X] ~~ClearArgument() error~~ (clipclear argument not supported) - - [X] ClearProcess(assignedContents string) error - - [X] CopyShortcut(realPath string, field int) error - - [X] CopyString(clearClipboardAutomatically bool, copySubject string) error - - [X] ~~LaunchClearProcess(copySubject string)~~ (clipclear argument not supported) - - [X] ~~TOTPCopier(secret string, errorChan chan<- error, done <-chan bool)~~ (not supported due to complexity) -- [X] config - - [X] Write(cfg *CfgT, appendMode bool) error - - [X] Load() (*CfgT, error) -- [X] core - - [X] EntryAddPrecheck(realPath string) (uint8, error) - - [X] EntryIsNotEmpty(entryData []string) bool - - [X] EntryRefresh(oldRCWPassword, newRCWPassword []byte, removeOldDir bool) error - - [X] GenTOTP(secret string, time time.Time) (string, error) - - [X] GetOldEntryData(realPath string, field int, rcwPassword []byte) ([]string, error) - - [X] ~~LibmuttonInit(inputCB func(prompt string) string, rcwPassword []byte, ...) error~~ (should be performed manually) - - [X] RCWSanityCheckGen(password []byte) error - - [X] VerifyEntries(rcwPassword []byte) error - - [X] WriteEntry(realPath string, decSlice []string, passwordIsNew bool, rcwPassword []byte) error -- [X] crypt - - [X] ~~VAR: RetryPassword bool~~ (RCWD not supported) - - [X] DecryptFileToSlice(realPath string) ([]string, error) - - [X] EncryptBytes(decBytes []byte) []byte - - [X] ~~RCWDArgument()~~ (RCWD not supported) -- [X] global - - [X] ~~VAR (CB func): GetPassword~~ (RCWD not supported) - - [X] VAR LibmuttonVersion (GetVersion()) - - [X] DirInit(preserveOldCfgDir bool) (string, error) - - [X] ~~GenDeviceIDList() ([]fs.DirEntry, error)~~ (not for use outside of libmutton) - - [X] GetCurrentDeviceID() (string, error) - - [X] GetRealAgePath(vanityPath string) string - - [X] GetRealPath(vanityPath string) string - - [X] ~~GetSysProcAttr() *syscall.SysProcAttr~~ (not for use outside of libmutton) - - [X] GetVanityPath(realPath string) string -- [X] syncclient - - [X] AddFolderRemote(vanityPath string) error - - [X] GenDeviceID(oldDeviceID, prefix string) (string, string, bool, error) - - [X] ~~GetSSHClient() (*ssh.Client, bool, *bool, *string, *string, error)~~ (not for use outside of libmutton) - - [X] ~~GetSSHOutput(sshClient *ssh.Client, cmd, stdin string) ([]byte, error)~~ (not for use outside of libmutton) - - [X] RenameRemote(oldVanityPath, newVanityPath string) error - - [X] ShearRemote(vanityPath string, onlyShearAgeFile bool) error - - [X] RunJob() (*syncListsT, error) -- [X] synccommon - - [X] ~~AddFolderLocal(vanityPath string) error~~ (not for use outside of libmutton) - - [X] ~~RenameLocal(oldVanityPath, newVanityPath string) error~~ (not for use outside of libmutton) - - [X] ~~ShearAgeFileLocal(vanityPath string) error~~ (not for use outside of libmutton) - - [X] ~~ShearLocal(vanityPath, clientDeviceID string, onlyShearAgeFile bool) (string, bool, error)~~ (not for use outside of libmutton) - - [X] WalkEntryDir() ([]string, []string, error) - - [X] GetAllEntryData() (EntryMapT, error) -- [X] syncserver - - [X] ~~GetRemoteDataFromServer(clientDeviceID string)~~ (not for use outside of libmutton) diff --git a/age.go b/age.go index a045eb3..c612c68 100644 --- a/age.go +++ b/age.go @@ -4,6 +4,8 @@ package main // #include "types.h" import "C" import ( + "unsafe" + "github.com/rwinkhart/libmutton/age" ) @@ -23,7 +25,7 @@ func AgeEntry(vanityPath C.PascalString, timestamp int64) *C.char { // //export AgeAllPasswordEntries func AgeAllPasswordEntries(forceReage bool, rcwPassword C.PascalString) *C.char { - if err := age.AllPasswordEntries(forceReage, []byte(C.GoStringN(rcwPassword.data, rcwPassword.len))); err != nil { + if err := age.AllPasswordEntries(forceReage, C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)); err != nil { return C.CString(err.Error()) } return nil diff --git a/clip.go b/clip.go index 4d61774..c95a2c8 100644 --- a/clip.go +++ b/clip.go @@ -6,6 +6,8 @@ package main // #include "types.h" import "C" import ( + "unsafe" + "github.com/rwinkhart/libmutton/clip" ) @@ -27,7 +29,7 @@ func ClearProcess(assignedContents C.PascalString) *C.char { // //export CopyShortcut func CopyShortcut(realPath C.PascalString, field int, rcwPassword C.PascalString) *C.char { - if err := clip.CopyShortcut(C.GoStringN(realPath.data, realPath.len), field, []byte(C.GoStringN(rcwPassword.data, rcwPassword.len))); err != nil { + if err := clip.CopyShortcut(C.GoStringN(realPath.data, realPath.len), field, C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)); err != nil { return C.CString(err.Error()) } return nil diff --git a/core.go b/core.go index 131dd8a..940626c 100644 --- a/core.go +++ b/core.go @@ -5,6 +5,7 @@ package main import "C" import ( "time" + "unsafe" "github.com/rwinkhart/libmutton/core" ) @@ -28,7 +29,7 @@ func EntryAddPrecheck(realPath C.PascalString) (*C.char, uint8) { // //export EntryIsNotEmpty func EntryIsNotEmpty(entryData C.PascalStringArray) bool { - notEmpty := core.EntryIsNotEmpty(getStringSliceFromCPascalStringArray(entryData)) + notEmpty := core.EntryIsNotEmpty(getStringSliceFromCPascalStringArrayAndFree(entryData)) return notEmpty } @@ -37,7 +38,7 @@ func EntryIsNotEmpty(entryData C.PascalStringArray) bool { // //export EntryRefresh func EntryRefresh(oldRCWPassword, newRCWPassword C.PascalString, removeOldDir bool) *C.char { - err := core.EntryRefresh([]byte(C.GoStringN(oldRCWPassword.data, oldRCWPassword.len)), []byte(C.GoStringN(newRCWPassword.data, newRCWPassword.len)), removeOldDir) + err := core.EntryRefresh(C.GoBytes(unsafe.Pointer(oldRCWPassword.data), oldRCWPassword.len), C.GoBytes(unsafe.Pointer(newRCWPassword.data), newRCWPassword.len), removeOldDir) if err != nil { return C.CString(err.Error()) } @@ -65,7 +66,7 @@ func GenTOTP(secret C.PascalString, unixTimestamp int64) (*C.char, C.PascalStrin // //export GetOldEntryData func GetOldEntryData(realPath C.PascalString, field int, rcwPassword C.PascalString) (*C.char, C.PascalStringArray) { - lines, err := core.GetOldEntryData(C.GoStringN(realPath.data, realPath.len), field, []byte(C.GoStringN(rcwPassword.data, rcwPassword.len))) + lines, err := core.GetOldEntryData(C.GoStringN(realPath.data, realPath.len), field, C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)) if err != nil { return C.CString(err.Error()), C.PascalStringArray{} } @@ -77,7 +78,7 @@ func GetOldEntryData(realPath C.PascalString, field int, rcwPassword C.PascalStr // //export RCWSanityCheckGen func RCWSanityCheckGen(password C.PascalString) *C.char { - err := core.RCWSanityCheckGen([]byte(C.GoStringN(password.data, password.len))) + err := core.RCWSanityCheckGen(C.GoBytes(unsafe.Pointer(password.data), password.len)) if err != nil { return C.CString(err.Error()) } @@ -89,7 +90,7 @@ func RCWSanityCheckGen(password C.PascalString) *C.char { // //export VerifyEntries func VerifyEntries(rcwPassword C.PascalString) *C.char { - err := core.VerifyEntries([]byte(C.GoStringN(rcwPassword.data, rcwPassword.len))) + err := core.VerifyEntries(C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)) if err != nil { return C.CString(err.Error()) } @@ -101,7 +102,7 @@ func VerifyEntries(rcwPassword C.PascalString) *C.char { // //export WriteEntry func WriteEntry(realPath C.PascalString, decSlice C.PascalStringArray, passwordIsNew bool, rcwPassword C.PascalString) *C.char { - if err := core.WriteEntry(C.GoStringN(realPath.data, realPath.len), getStringSliceFromCPascalStringArray(decSlice), passwordIsNew, []byte(C.GoStringN(rcwPassword.data, rcwPassword.len))); err != nil { + if err := core.WriteEntry(C.GoStringN(realPath.data, realPath.len), getStringSliceFromCPascalStringArrayAndFree(decSlice), passwordIsNew, C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)); err != nil { return C.CString(err.Error()) } return nil diff --git a/crypt.go b/crypt.go index 6e1b826..e7123a2 100644 --- a/crypt.go +++ b/crypt.go @@ -4,6 +4,8 @@ package main // #include "types.h" import "C" import ( + "unsafe" + "github.com/rwinkhart/libmutton/crypt" "github.com/rwinkhart/rcw/wrappers" ) @@ -15,7 +17,7 @@ import ( // //export DecryptFileToSlice func DecryptFileToSlice(realPath, rcwPassword C.PascalString) (*C.char, C.PascalStringArray) { - lines, err := crypt.DecryptFileToSlice(C.GoStringN(realPath.data, realPath.len), []byte(C.GoStringN(rcwPassword.data, rcwPassword.len))) + lines, err := crypt.DecryptFileToSlice(C.GoStringN(realPath.data, realPath.len), C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)) if err != nil { return C.CString(err.Error()), C.PascalStringArray{} } @@ -33,6 +35,6 @@ func DecryptFileToSlice(realPath, rcwPassword C.PascalString) (*C.char, C.Pascal //export EncryptBytes func EncryptBytes(decBytes, rcwPassword C.PascalString) C.PascalString { // use wrappers.Encrypt directly since C bindings do no support the RCWD daemon - encBytes := wrappers.Encrypt([]byte(C.GoStringN(decBytes.data, decBytes.len)), []byte(C.GoStringN(rcwPassword.data, rcwPassword.len))) + encBytes := wrappers.Encrypt(C.GoBytes(unsafe.Pointer(decBytes.data), decBytes.len), C.GoBytes(unsafe.Pointer(rcwPassword.data), rcwPassword.len)) return getPascalStringFromBytes(encBytes) } diff --git a/go.mod b/go.mod index 1538196..60f4f07 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module cmutton go 1.26rc3 require ( - github.com/rwinkhart/libmutton v0.4.3-0.20260208003646-25edd344c861 + github.com/rwinkhart/go-boilerplate v0.2.3-0.20260208035402-e1bb0012a781 + github.com/rwinkhart/libmutton v0.4.3-0.20260208040332-7bfbc7ef309e github.com/rwinkhart/rcw v0.2.5 ) @@ -13,7 +14,6 @@ require ( github.com/kr/fs v0.1.0 // indirect github.com/pkg/sftp v1.13.10 // indirect github.com/pquerna/otp v1.5.0 // indirect - github.com/rwinkhart/go-boilerplate v0.2.2 // indirect github.com/rwinkhart/peercred-mini v0.1.2 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 94955e9..0e71ef9 100644 --- a/go.sum +++ b/go.sum @@ -14,10 +14,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -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/libmutton v0.4.3-0.20260208003646-25edd344c861 h1:eDEH43Ch0/tSoAU+t2c4QYK9B3d4gMi9LVqUfr8ET5E= -github.com/rwinkhart/libmutton v0.4.3-0.20260208003646-25edd344c861/go.mod h1:Z1QDejdTmI/ydQtnu5jUYZszAeT30AtXPDm0wkowAu0= +github.com/rwinkhart/go-boilerplate v0.2.3-0.20260208035402-e1bb0012a781 h1:kMAM0fkWGPa8WK7A4a9rMX2oD2Lv198zJoD9LCUAzOA= +github.com/rwinkhart/go-boilerplate v0.2.3-0.20260208035402-e1bb0012a781/go.mod h1:QcUlr4SMgONRMKD965m8oTuCa6ZJVAc17w27Qopyr+s= +github.com/rwinkhart/libmutton v0.4.3-0.20260208040332-7bfbc7ef309e h1:Hl/LG+vc7RkU4tHVq+rMt15zg4t3utDZN6HY5RfPDPY= +github.com/rwinkhart/libmutton v0.4.3-0.20260208040332-7bfbc7ef309e/go.mod h1:G9EvjsHvTyOvUOs1VZ8aRKRpRRgzrrdZSCGheL/t72I= 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/rcw v0.2.5 h1:3GJeii9sDZsZNL+HRkfHrnbgxnaB8a9kejiMIfPXnpE= diff --git a/privkey.go b/privkey.go new file mode 100644 index 0000000..b7e591b --- /dev/null +++ b/privkey.go @@ -0,0 +1,51 @@ +package main + +// #include +// #include +// #include "types.h" +import "C" +import ( + "unsafe" + + "github.com/rwinkhart/go-boilerplate/back" + "github.com/rwinkhart/libmutton/privkey" +) + +// SetPrivKeyDataAndEraseFreeInput is meant to be called on iOS +// prior to any server connections to ensure libmutton can +// retrieve the correct SSH private key contents (enables +// loading from keychain). +// +//export SetPrivKeyDataAndEraseFreeInput +func SetPrivKeyDataAndEraseFreeInput(privKeyData C.PascalString) { + privKeyDataPtr := unsafe.Pointer(privKeyData.data) + privkey.SetBytes(C.GoBytes(privKeyDataPtr, privKeyData.len)) + securePtrOverwriteAndFree(privKeyDataPtr, privKeyData.len) + FreePascalString(privKeyData) +} + +// GetPrivKeyDataAndEraseGoCopy will return nil on non-iOS platforms. +// On iOS, it will return whatever was set in +// SetPrivKeyDataAndSecurelyFreeInput. +// +// Don't forget to call EraseFreePrivKeyPascal on the output +// of this function as soon as it is no longer needed. +// +//export GetPrivKeyDataAndEraseGoCopy +func GetPrivKeyDataAndEraseGoCopy() C.PascalString { + privKeyBytes, _ := privkey.GetBytes(nil) + privKeyPascal := getPascalStringFromBytes(privKeyBytes) + back.EraseBytesSecurely(privKeyBytes) + return privKeyPascal +} + +// EraseFreePrivKeyPascal is meant to be called on +// iOS after any server connections to ensure the cached SSH private key +// set by SetPrivKeyData is securely erased. It also erases and frees +// the input privKeyData C.PascalString. +// +//export EraseFreePrivKeyPascal +func EraseFreePrivKeyPascal(privKeyPascal C.PascalString) { + securePtrOverwriteAndFree(unsafe.Pointer(privKeyPascal.data), privKeyPascal.len) + FreePascalString(privKeyPascal) +}