mirror of
https://github.com/rwinkhart/MUTN.git
synced 2026-08-28 04:46:41 -04:00
Port to latest libmutton development version & begin migration from GPG to RCW
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
/mutn
|
/mutn
|
||||||
/mutn.exe
|
/mutn.exe
|
||||||
|
/MUTN
|
||||||
|
/MUTN.exe
|
||||||
/main
|
/main
|
||||||
/main.exe
|
/main.exe
|
||||||
/version.go
|
/version.go
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ go 1.24.2
|
|||||||
require (
|
require (
|
||||||
github.com/Trojan2021/BEAN v0.0.0-20241210230804-8f294833b514
|
github.com/Trojan2021/BEAN v0.0.0-20241210230804-8f294833b514
|
||||||
github.com/charmbracelet/glamour v0.7.0
|
github.com/charmbracelet/glamour v0.7.0
|
||||||
github.com/rwinkhart/libmutton v0.3.1
|
github.com/rwinkhart/libmutton v0.3.2-0.20250504220509-826d94bc4397
|
||||||
|
github.com/rwinkhart/rcw v0.0.0-20250504183741-f176d57b6ba5
|
||||||
golang.org/x/term v0.31.0
|
golang.org/x/term v0.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,3 +43,7 @@ require (
|
|||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace golang.org/x/sys => github.com/rwinkhart/sys-freebsd-13-xucred v0.32.0
|
||||||
|
|
||||||
|
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410
|
||||||
|
|||||||
@@ -59,8 +59,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rwinkhart/convertroman v0.2.0 h1:otUm939eXT77q/Lr31mJtMwVWEw0RjEUB71gO65h9OM=
|
github.com/rwinkhart/convertroman v0.2.0 h1:otUm939eXT77q/Lr31mJtMwVWEw0RjEUB71gO65h9OM=
|
||||||
github.com/rwinkhart/convertroman v0.2.0/go.mod h1:Af6HqvX0EIM4Y3HcnNXbbRey1dLasGB7WU0+3Cowgmw=
|
github.com/rwinkhart/convertroman v0.2.0/go.mod h1:Af6HqvX0EIM4Y3HcnNXbbRey1dLasGB7WU0+3Cowgmw=
|
||||||
github.com/rwinkhart/libmutton v0.3.1 h1:VCaviTYLO8dif75ZjOJh1EZoaM2zH5U1CaYOa0qkW/E=
|
github.com/rwinkhart/libmutton v0.3.2-0.20250504220509-826d94bc4397 h1:+Zlv62ZB7QTIFDNltMj6rm+7huo05N99BBWhoyVVJQc=
|
||||||
github.com/rwinkhart/libmutton v0.3.1/go.mod h1:bI03SUAheTefZYcEv19Vg7hWsEamd3qjqTqlB/Z6YSw=
|
github.com/rwinkhart/libmutton v0.3.2-0.20250504220509-826d94bc4397/go.mod h1:9a4EYsPTLVI4Je7ql3jsMz7B9JP4PaRZSuTR/BctBIU=
|
||||||
|
github.com/rwinkhart/rcw v0.0.0-20250504183741-f176d57b6ba5 h1:BX31d76TLGZqeukYECLFPkLD+SS/CAV12Lkdlc6UuGM=
|
||||||
|
github.com/rwinkhart/rcw v0.0.0-20250504183741-f176d57b6ba5/go.mod h1:giXrq9o5a7bwMSuMvz+5bb2+qbM+zV4pXaG/SceXfLM=
|
||||||
|
github.com/rwinkhart/sys-freebsd-13-xucred v0.32.0 h1:KRbqimv9Eexf3VB2FrRAQ4v2fGGu7gt3ayMgdT0FNao=
|
||||||
|
github.com/rwinkhart/sys-freebsd-13-xucred v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -111,20 +115,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func main() {
|
|||||||
case "show", "-s":
|
case "show", "-s":
|
||||||
cli.EntryReaderDecrypt(targetLocation, false)
|
cli.EntryReaderDecrypt(targetLocation, false)
|
||||||
case "copy":
|
case "copy":
|
||||||
core.CopyArgument(targetLocation, 0)
|
core.CopyArgument(targetLocation, 0, cli.GetRCWPassphrase())
|
||||||
case "edit":
|
case "edit":
|
||||||
cli.EditEntryField(targetLocation, true, 0)
|
cli.EditEntryField(targetLocation, true, 0)
|
||||||
case "gen":
|
case "gen":
|
||||||
@@ -81,7 +81,7 @@ func main() {
|
|||||||
default:
|
default:
|
||||||
cli.HelpCopy()
|
cli.HelpCopy()
|
||||||
}
|
}
|
||||||
core.CopyArgument(targetLocation, field)
|
core.CopyArgument(targetLocation, field, cli.GetRCWPassphrase())
|
||||||
case "edit":
|
case "edit":
|
||||||
var field int // indicates which (numbered) field to edit
|
var field int // indicates which (numbered) field to edit
|
||||||
switch args[3] {
|
switch args[3] {
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MUTNVersion = "0.2.6" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.2.A" -> "0.2.0", "0.2.B" -> "0.2.1"
|
MUTNVersion = "0.D.0" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.2.A" -> "0.2.0", "0.2.B" -> "0.2.1"
|
||||||
|
|
||||||
AnsiBold = "\033[1m"
|
AnsiBold = "\033[1m"
|
||||||
ansiBlackOnWhite = "\033[38;5;0;48;5;15m"
|
ansiBlackOnWhite = "\033[38;5;0;48;5;15m"
|
||||||
|
|||||||
+4
-4
@@ -22,16 +22,16 @@ func RenameCli(oldLocationIncomplete string) {
|
|||||||
// EditEntryField edits a field of an entry at targetLocation (user input).
|
// EditEntryField edits a field of an entry at targetLocation (user input).
|
||||||
func EditEntryField(targetLocation string, hideSecrets bool, field int) {
|
func EditEntryField(targetLocation string, hideSecrets bool, field int) {
|
||||||
// fetch old entry data (with all required lines present)
|
// fetch old entry data (with all required lines present)
|
||||||
unencryptedEntry := core.GetOldEntryData(targetLocation, field)
|
unencryptedEntry := core.GetOldEntryData(targetLocation, field, GetRCWPassphrase())
|
||||||
|
|
||||||
// edit the field
|
// edit the field
|
||||||
switch field {
|
switch field {
|
||||||
case 0:
|
case 0:
|
||||||
unencryptedEntry[field] = string(inputHidden("Password:"))
|
unencryptedEntry[field] = string(GetRCWPassphrase())
|
||||||
case 1:
|
case 1:
|
||||||
unencryptedEntry[field] = input("Username:")
|
unencryptedEntry[field] = input("Username:")
|
||||||
case 2:
|
case 2:
|
||||||
unencryptedEntry[field] = string(inputHidden("TOTP secret:"))
|
unencryptedEntry[field] = string(GetRCWPassphrase())
|
||||||
case 3:
|
case 3:
|
||||||
unencryptedEntry[field] = input("URL:")
|
unencryptedEntry[field] = input("URL:")
|
||||||
case 4: // edit notes fields
|
case 4: // edit notes fields
|
||||||
@@ -54,7 +54,7 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) {
|
|||||||
// GenUpdate generates a new password for an entry at targetLocation (user input).
|
// GenUpdate generates a new password for an entry at targetLocation (user input).
|
||||||
func GenUpdate(targetLocation string, hideSecrets bool) {
|
func GenUpdate(targetLocation string, hideSecrets bool) {
|
||||||
// fetch old entry data
|
// fetch old entry data
|
||||||
unencryptedEntry := core.GetOldEntryData(targetLocation, 0)
|
unencryptedEntry := core.GetOldEntryData(targetLocation, 0, GetRCWPassphrase())
|
||||||
|
|
||||||
// generate a new password
|
// generate a new password
|
||||||
unencryptedEntry[0] = inputPasswordGen()
|
unencryptedEntry[0] = inputPasswordGen()
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts a GPG-encrypted file before sending it to EntryReader.
|
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader.
|
||||||
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) {
|
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) {
|
||||||
if isFile, _ := core.TargetIsFile(targetLocation, true, 2); isFile {
|
if isFile, _ := core.TargetIsFile(targetLocation, true, 2); isFile {
|
||||||
EntryReader(core.DecryptGPG(targetLocation), hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified
|
EntryReader(core.DecryptFileToSlice(targetLocation, GetRCWPassphrase()), hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified
|
||||||
}
|
}
|
||||||
// do not exit, as this is the job of EntryReader
|
// do not exit, as this is the job of EntryReader
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-17
@@ -1,6 +1,7 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,21 +13,7 @@ import (
|
|||||||
|
|
||||||
// TempInitCli initializes the MUTN environment based on user input.
|
// TempInitCli initializes the MUTN environment based on user input.
|
||||||
func TempInitCli() {
|
func TempInitCli() {
|
||||||
// gpgID
|
// text editor
|
||||||
var gpgID string
|
|
||||||
if inputBinary("Auto-generate GPG key?") {
|
|
||||||
gpgID = core.GpgKeyGen()
|
|
||||||
} else {
|
|
||||||
// select GPG key from menu
|
|
||||||
uidSlice := core.GpgUIDListGen()
|
|
||||||
gpgIDInt := inputMenuGen("Select GPG key:", uidSlice)
|
|
||||||
if gpgIDInt == 0 {
|
|
||||||
core.PrintError("No GPG keys found - please generate one", core.ErrorTargetNotFound, true)
|
|
||||||
}
|
|
||||||
gpgID = uidSlice[gpgIDInt-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// textEditor
|
|
||||||
textEditor := cmp.Or(input("Text editor (leave blank for $EDITOR, falls back to \""+fallbackEditor+"\"):"), os.Getenv("EDITOR"), fallbackEditor)
|
textEditor := cmp.Or(input("Text editor (leave blank for $EDITOR, falls back to \""+fallbackEditor+"\"):"), os.Getenv("EDITOR"), fallbackEditor)
|
||||||
|
|
||||||
// SSH info
|
// SSH info
|
||||||
@@ -56,7 +43,7 @@ func TempInitCli() {
|
|||||||
oldDeviceID := core.DirInit(false)
|
oldDeviceID := core.DirInit(false)
|
||||||
|
|
||||||
// write config file (temporarily assigns sshEntryRoot and sshIsWindows to null to pass initial device ID registration)
|
// write config file (temporarily assigns sshEntryRoot and sshIsWindows to null to pass initial device ID registration)
|
||||||
core.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "gpgID", gpgID}, {"LIBMUTTON", "sshUser", sshUser}, {"LIBMUTTON", "sshIP", sshIP}, {"LIBMUTTON", "sshPort", sshPort}, {"LIBMUTTON", "sshKey", sshKey}, {"LIBMUTTON", "sshKeyProtected", strconv.FormatBool(sshKeyProtected)}, {"LIBMUTTON", "sshEntryRoot", "null"}, {"LIBMUTTON", "sshIsWindows", "false"}}, nil, false)
|
core.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "sshUser", sshUser}, {"LIBMUTTON", "sshIP", sshIP}, {"LIBMUTTON", "sshPort", sshPort}, {"LIBMUTTON", "sshKey", sshKey}, {"LIBMUTTON", "sshKeyProtected", strconv.FormatBool(sshKeyProtected)}, {"LIBMUTTON", "sshEntryRoot", "null"}, {"LIBMUTTON", "sshIsWindows", "false"}}, nil, false)
|
||||||
|
|
||||||
// generate and register device ID
|
// generate and register device ID
|
||||||
sshEntryRoot, sshIsWindows := sync.DeviceIDGen(oldDeviceID)
|
sshEntryRoot, sshIsWindows := sync.DeviceIDGen(oldDeviceID)
|
||||||
@@ -68,6 +55,18 @@ func TempInitCli() {
|
|||||||
core.DirInit(false)
|
core.DirInit(false)
|
||||||
|
|
||||||
// write config file
|
// write config file
|
||||||
core.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}, {"LIBMUTTON", "gpgID", gpgID}}, nil, false)
|
core.WriteConfig([][3]string{{"MUTN", "textEditor", textEditor}}, nil, false)
|
||||||
|
}
|
||||||
|
// RCW sanity check file
|
||||||
|
var passphraseInput1 []byte
|
||||||
|
for {
|
||||||
|
passphraseInput1 = inputHidden("New master (RCW) passphrase:")
|
||||||
|
passphraseInput2 := inputHidden("Confirm RCW passphrase:")
|
||||||
|
if !bytes.Equal(passphraseInput1, passphraseInput2) {
|
||||||
|
fmt.Println(core.AnsiError + "Passphrases do not match" + core.AnsiReset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
core.RCWSanityCheckGen(passphraseInput1)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,24 @@ import (
|
|||||||
|
|
||||||
"github.com/rwinkhart/libmutton/core"
|
"github.com/rwinkhart/libmutton/core"
|
||||||
"github.com/rwinkhart/libmutton/sync"
|
"github.com/rwinkhart/libmutton/sync"
|
||||||
|
"github.com/rwinkhart/rcw/wrappers"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetRCWPassphrase retrieves the RCW passphrase from the user.
|
||||||
|
// TODO attempt to retrieve from RCW daemon
|
||||||
|
func GetRCWPassphrase() []byte {
|
||||||
|
for {
|
||||||
|
passphrase := inputHidden("RCW Passphrase:")
|
||||||
|
err := wrappers.RunSanityCheck(core.ConfigDir+"/sanity.rcw", passphrase)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(core.AnsiError + "Failed to encrypt - " + err.Error() + core.AnsiReset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return passphrase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// input prompts the user for input and returns the input as a string.
|
// input prompts the user for input and returns the input as a string.
|
||||||
func input(prompt string) string {
|
func input(prompt string) string {
|
||||||
fmt.Print("\n" + prompt + " ")
|
fmt.Print("\n" + prompt + " ")
|
||||||
@@ -85,7 +100,8 @@ func inputPasswordGen() string {
|
|||||||
func writeEntryCLI(targetLocation string, unencryptedEntry []string, hideSecrets bool) {
|
func writeEntryCLI(targetLocation string, unencryptedEntry []string, hideSecrets bool) {
|
||||||
if core.EntryIsNotEmpty(unencryptedEntry) {
|
if core.EntryIsNotEmpty(unencryptedEntry) {
|
||||||
// write the entry to the target location
|
// write the entry to the target location
|
||||||
core.WriteEntry(targetLocation, unencryptedEntry)
|
joinedUnencryptedEntry := strings.Join(unencryptedEntry, "\n")
|
||||||
|
core.WriteEntry(targetLocation, []byte(joinedUnencryptedEntry), GetRCWPassphrase())
|
||||||
// preview the entry
|
// preview the entry
|
||||||
fmt.Println(AnsiBold + "\nEntry Preview:" + core.AnsiReset)
|
fmt.Println(AnsiBold + "\nEntry Preview:" + core.AnsiReset)
|
||||||
EntryReader(unencryptedEntry, hideSecrets, true)
|
EntryReader(unencryptedEntry, hideSecrets, true)
|
||||||
|
|||||||
Reference in New Issue
Block a user