mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af6e11adc | ||
|
|
a3a98858fd | ||
|
|
d0652f2fd3 | ||
|
|
936f067363 | ||
|
|
041c3c7048 | ||
|
|
b48f74af58 | ||
|
|
6ec8849b20 | ||
|
|
db7cf1ed62 | ||
|
|
16e1e77068 | ||
|
|
1cfa694aa2 | ||
|
|
5898877136 | ||
|
|
a7d8cac565 | ||
|
|
0e73afca47 | ||
|
|
432941d694 | ||
|
|
ac521bf53c | ||
|
|
9720c9f14f | ||
|
|
4a224fdb9c | ||
|
|
671eb4d777 | ||
|
|
16cf816610 | ||
|
|
879f576c6e | ||
|
|
2df78cdbf5 | ||
|
|
a05ffc0ae1 | ||
|
|
6b4b218f09 | ||
|
|
4d0d46afd6 | ||
|
|
f24c057503 | ||
|
|
7e3c1f1549 | ||
|
|
1d4c0ec8a5 | ||
|
|
92dda6aa4f | ||
|
|
9a605dfda6 | ||
|
|
e918885556 | ||
|
|
263702594f | ||
|
|
72b8dfb454 | ||
|
|
aa2383ac24 | ||
|
|
b6f0ea013d | ||
|
|
eb82e18a52 | ||
|
|
40ea51b685 | ||
|
|
1285b44c50 | ||
|
|
cb63a3378f | ||
|
|
4f90bfdcda | ||
|
|
5bb1dcc137 | ||
|
|
57c417c48f | ||
|
|
55fc59f6f5 | ||
|
|
332852d095 | ||
|
|
a903d2f596 | ||
|
|
4a4bb683e3 | ||
|
|
016299c756 | ||
|
|
075caaccda | ||
|
|
729be253d3 | ||
|
|
13bd48182f | ||
|
|
67188dabea | ||
|
|
6e4c38c10e | ||
|
|
00ecb1f4fd | ||
|
|
bac047fedd | ||
|
|
10c16be351 | ||
|
|
d51ea9dbab | ||
|
|
0eb41094de | ||
|
|
a219a97cb4 | ||
|
|
b617bb4cf4 | ||
|
|
613b53430b | ||
|
|
472de54757 | ||
|
|
bdbc3bd7b5 | ||
|
|
87db61bcfb | ||
|
|
095eb2722b | ||
|
|
abbb0da434 | ||
|
|
52f7484a71 | ||
|
|
a79b5fd7b9 | ||
|
|
3e98d2d7a1 | ||
|
|
13bf9d0a01 | ||
|
|
7aae725b88 | ||
|
|
afd01073e4 | ||
|
|
ae0c33f41b | ||
|
|
18da00b449 | ||
|
|
dc736f6095 | ||
|
|
e02d53ce67 | ||
|
|
064cd6a2cc | ||
|
|
b7c336cb76 |
No files matched your search
@@ -5,6 +5,7 @@
|
||||
/artifacts
|
||||
/artifacts/**
|
||||
|
||||
*.rc
|
||||
|
||||
/Dockerfile.*
|
||||
/*.sdk.tar.xz
|
||||
|
||||
@@ -39,10 +39,6 @@ Please checkout the [wiki](https://sliver.sh/), or start a [GitHub discussion](h
|
||||
|
||||
See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
|
||||
### Feedback
|
||||
|
||||
Please take a moment and fill out [our survey](https://forms.gle/SwVsHFNh24ChG58C6).
|
||||
|
||||
### License - GPLv3
|
||||
|
||||
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
|
||||
|
||||
@@ -51,6 +51,7 @@ func init() {
|
||||
defer logFile.Close()
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
// This created before anything so that multiple commands can make use of
|
||||
|
||||
@@ -40,6 +40,7 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
Short: "Start the sliver client console",
|
||||
}
|
||||
|
||||
consoleCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
consoleCmd.RunE, consoleCmd.PersistentPostRunE = consoleRunnerCmd(con, true)
|
||||
return consoleCmd
|
||||
}
|
||||
@@ -47,7 +48,7 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
pre = func(_ *cobra.Command, _ []string) error {
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
@@ -59,13 +60,18 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
return nil
|
||||
}
|
||||
|
||||
rcScript, err := ReadRCScript(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read rc script: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't clobber output when simply running an implant command from system shell.
|
||||
if run {
|
||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
||||
}
|
||||
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var err error
|
||||
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
@@ -76,7 +82,7 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
// Wait for any connection state changes and exit if the connection is lost.
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run)
|
||||
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
}
|
||||
|
||||
// Close the RPC connection once exiting
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const RCFlagName = "rc"
|
||||
|
||||
func ReadRCScript(cmd *cobra.Command) (string, error) {
|
||||
if cmd == nil {
|
||||
return "", nil
|
||||
}
|
||||
if cmd.Flags().Lookup(RCFlagName) == nil {
|
||||
return "", nil
|
||||
}
|
||||
rcPath, err := cmd.Flags().GetString(RCFlagName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rcPath == "" {
|
||||
return "", nil
|
||||
}
|
||||
data, err := os.ReadFile(rcPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read rc script %q: %w", rcPath, err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/carapace-sh/carapace"
|
||||
app "github.com/reeflective/console"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -76,6 +77,16 @@ type AliasFile struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// AliasArgument - An argument for an alias command.
|
||||
type AliasArgument struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Desc string `json:"desc"`
|
||||
Optional bool `json:"optional"`
|
||||
Default interface{} `json:"default,omitempty"`
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
}
|
||||
|
||||
// AliasManifest - The manifest for an alias, contains metadata.
|
||||
type AliasManifest struct {
|
||||
Name string `json:"name"`
|
||||
@@ -89,6 +100,7 @@ type AliasManifest struct {
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
AllowArgs bool `json:"allow_args"`
|
||||
DefaultArgs string `json:"default_args"`
|
||||
Arguments []*AliasArgument `json:"arguments"`
|
||||
Files []*AliasFile `json:"files"`
|
||||
IsReflective bool `json:"is_reflective"`
|
||||
IsAssembly bool `json:"is_assembly"`
|
||||
@@ -158,15 +170,60 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
}
|
||||
aliasManifest.RootPath = filepath.Dir(manifestPath)
|
||||
|
||||
// Build usage string including arguments
|
||||
usage := strings.Builder{}
|
||||
usage.WriteString(aliasManifest.CommandName)
|
||||
for _, arg := range aliasManifest.Arguments {
|
||||
usage.WriteString(" ")
|
||||
if arg.Optional {
|
||||
usage.WriteString("[")
|
||||
}
|
||||
usage.WriteString(strings.ToUpper(arg.Name))
|
||||
if arg.Optional {
|
||||
usage.WriteString("]")
|
||||
}
|
||||
}
|
||||
|
||||
// Build long help message
|
||||
longHelp := strings.Builder{}
|
||||
longHelp.WriteString("[[.Bold]]Command:[[.Normal]]")
|
||||
longHelp.WriteString(usage.String())
|
||||
longHelp.WriteString("\n")
|
||||
if len(aliasManifest.Help) > 0 || len(aliasManifest.LongHelp) > 0 {
|
||||
longHelp.WriteString("[[.Bold]]About:[[.Normal]]")
|
||||
if len(aliasManifest.Help) > 0 {
|
||||
longHelp.WriteString(aliasManifest.Help)
|
||||
longHelp.WriteString("\n")
|
||||
}
|
||||
if len(aliasManifest.LongHelp) > 0 {
|
||||
longHelp.WriteString(aliasManifest.LongHelp)
|
||||
longHelp.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if len(aliasManifest.Arguments) > 0 {
|
||||
longHelp.WriteString("[[.Bold]]Arguments:[[.Normal]]")
|
||||
for _, arg := range aliasManifest.Arguments {
|
||||
longHelp.WriteString("\n\t")
|
||||
optStr := ""
|
||||
if arg.Optional {
|
||||
optStr = "[OPTIONAL]"
|
||||
}
|
||||
aType := arg.Type
|
||||
if aType == "wstring" {
|
||||
aType = "string"
|
||||
}
|
||||
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
|
||||
}
|
||||
}
|
||||
longHelp.WriteString("\n\n⚠️ If you're having issues passing arguments to the alias please read:\n")
|
||||
longHelp.WriteString("https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing")
|
||||
|
||||
// for each alias command, add a new app command
|
||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
||||
longHelpMsg := help.FormatHelpTmpl(aliasManifest.LongHelp)
|
||||
longHelpMsg += "\n\n⚠️ If you're having issues passing arguments to the alias please read:\n"
|
||||
longHelpMsg += "https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing"
|
||||
addAliasCmd := &cobra.Command{
|
||||
Use: aliasManifest.CommandName,
|
||||
Use: usage.String(),
|
||||
Short: helpMsg,
|
||||
Long: longHelpMsg,
|
||||
Long: help.FormatHelpTmpl(longHelp.String()),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runAliasCommand(cmd, con, args)
|
||||
},
|
||||
@@ -196,6 +253,10 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
addAliasCmd.Flags().AddFlagSet(f)
|
||||
|
||||
// Setup completions for alias arguments
|
||||
comps := carapace.Gen(addAliasCmd)
|
||||
makeAliasArgCompleter(aliasManifest, comps)
|
||||
|
||||
cmd.AddCommand(addAliasCmd)
|
||||
|
||||
// Have to use a global map here, as passing the aliasCmd
|
||||
@@ -589,3 +650,105 @@ func makeAliasPlatformFilters(alias *AliasManifest) map[string]string {
|
||||
app.CommandFilterKey: strings.Join(all, ","),
|
||||
}
|
||||
}
|
||||
|
||||
// makeAliasArgCompleter builds the positional and dash arguments completer for the alias.
|
||||
// It provides completion for:
|
||||
// 1. Positional arguments (before --)
|
||||
// 2. Flag-style arguments after -- (e.g., --target, --port)
|
||||
func makeAliasArgCompleter(alias *AliasManifest, comps *carapace.Carapace) {
|
||||
if len(alias.Arguments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var actions []carapace.Action
|
||||
|
||||
for _, arg := range alias.Arguments {
|
||||
var action carapace.Action
|
||||
|
||||
// If choices are defined, use them for completion
|
||||
if len(arg.Choices) > 0 {
|
||||
action = carapace.ActionValues(arg.Choices...).Tag("choices")
|
||||
} else {
|
||||
// Fall back to type-based completion
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
action = carapace.ActionFiles().Tag("alias data")
|
||||
default:
|
||||
action = carapace.ActionValues()
|
||||
}
|
||||
}
|
||||
|
||||
usage := fmt.Sprintf("(%s) %s", arg.Type, arg.Desc)
|
||||
if arg.Optional {
|
||||
usage += " (optional)"
|
||||
}
|
||||
|
||||
actions = append(actions, action.Usage("%s", usage))
|
||||
}
|
||||
|
||||
comps.PositionalCompletion(actions...)
|
||||
|
||||
// Add dash completion for flag-style arguments after --
|
||||
// Pre-build the flag completions at registration time (not in a callback)
|
||||
flagCompletion := makeAliasFlagNameCompletion(alias)
|
||||
|
||||
// Build value completions for each argument type
|
||||
valueCompletions := make(map[string]carapace.Action)
|
||||
for _, arg := range alias.Arguments {
|
||||
// If choices are defined, use them for completion
|
||||
if len(arg.Choices) > 0 {
|
||||
valueCompletions[arg.Name] = carapace.ActionValues(arg.Choices...).Tag("choices")
|
||||
} else {
|
||||
// Fall back to type-based completion
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
valueCompletions[arg.Name] = carapace.ActionFiles().Tag("file path")
|
||||
case "bool":
|
||||
valueCompletions[arg.Name] = carapace.ActionValues("true", "false").Tag("boolean")
|
||||
default:
|
||||
valueCompletions[arg.Name] = carapace.ActionValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use DashAnyCompletion with a smart action that determines context
|
||||
comps.DashAnyCompletion(
|
||||
carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
// If typing a flag (starts with -)
|
||||
if strings.HasPrefix(c.Value, "-") {
|
||||
return flagCompletion
|
||||
}
|
||||
|
||||
// If previous arg was a flag, complete its value
|
||||
if len(c.Args) > 0 {
|
||||
lastArg := c.Args[len(c.Args)-1]
|
||||
if strings.HasPrefix(lastArg, "-") {
|
||||
flagName := strings.TrimLeft(lastArg, "-")
|
||||
if action, ok := valueCompletions[flagName]; ok {
|
||||
return action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: show flag names
|
||||
return flagCompletion
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// makeAliasFlagNameCompletion creates completion for flag names
|
||||
func makeAliasFlagNameCompletion(alias *AliasManifest) carapace.Action {
|
||||
var results []string
|
||||
|
||||
for _, arg := range alias.Arguments {
|
||||
flagName := fmt.Sprintf("--%s", arg.Name)
|
||||
desc := arg.Desc
|
||||
if arg.Optional {
|
||||
desc = fmt.Sprintf("[optional] %s", desc)
|
||||
}
|
||||
desc = fmt.Sprintf("(%s) %s", arg.Type, desc)
|
||||
results = append(results, flagName, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("alias arguments")
|
||||
}
|
||||
@@ -274,19 +274,22 @@ func getInstalledPackageNames() []string {
|
||||
}
|
||||
|
||||
// This is a convenience function to get the names of the commands in the cache
|
||||
func getCommandsInCache() []string {
|
||||
func getCommandsInCache(armoryPK string) []string {
|
||||
commandNames := []string{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
|
||||
return true
|
||||
}
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
|
||||
if cacheEntry.Alias.CommandName != "" && !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
|
||||
commandNames = append(commandNames, cacheEntry.Alias.CommandName)
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if !slices.Contains(commandNames, command.CommandName) {
|
||||
if command.CommandName != "" && !slices.Contains(commandNames, command.CommandName) {
|
||||
commandNames = append(commandNames, command.CommandName)
|
||||
}
|
||||
}
|
||||
@@ -398,7 +401,7 @@ func buildInstallList(name, armoryPK string, forceInstallation bool, pendingPack
|
||||
var requestedPackageList []string
|
||||
if name == "all" {
|
||||
requestedPackageList = []string{}
|
||||
allCommands := getCommandsInCache()
|
||||
allCommands := getCommandsInCache(armoryPK)
|
||||
for _, cmdName := range allCommands {
|
||||
if !slices.Contains(installedPackages, cmdName) || forceInstallation {
|
||||
// Check to see if there is a package pending with that name
|
||||
@@ -458,6 +461,15 @@ func installPackageByName(name, armoryPK string, forceInstallation, promptToOver
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(packageInstallList) == 0 && name == "all" {
|
||||
availableCommands := getCommandsInCache(armoryPK)
|
||||
if len(availableCommands) == 0 {
|
||||
con.PrintInfof("No packages or bundles found\n")
|
||||
} else {
|
||||
con.PrintInfof("All available packages are already installed\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(packageInstallList) > 0 {
|
||||
for _, packageID := range packageInstallList {
|
||||
entry := packageCacheLookupByID(packageID)
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
killFlag, _ := cmd.Flags().GetString("kill")
|
||||
killAll, _ := cmd.Flags().GetBool("kill-all")
|
||||
interact, _ := cmd.Flags().GetString("interact")
|
||||
|
||||
// Handle kill
|
||||
if killFlag != "" {
|
||||
@@ -72,6 +73,18 @@ func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintInfof("Killed %s (%s)\n", beacon.Name, beacon.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if interact != "" {
|
||||
beacon, err := GetBeacon(con, interact)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.ActiveTarget.Set(nil, beacon)
|
||||
con.PrintInfof("Active beacon %s (%s)\n", beacon.Name, strings.Split(beacon.ID, "-")[0])
|
||||
return
|
||||
}
|
||||
|
||||
filter, _ := cmd.Flags().GetString("filter")
|
||||
var filterRegex *regexp.Regexp
|
||||
if filterRe, _ := cmd.Flags().GetString("filter-re"); filterRe != "" {
|
||||
|
||||
@@ -33,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("kill", "k", "", "kill the designated beacon")
|
||||
f.BoolP("kill-all", "K", false, "kill all beacons")
|
||||
f.StringP("interact", "i", "", "interact with a beacon")
|
||||
f.BoolP("force", "F", false, "force killing the beacon")
|
||||
|
||||
f.StringP("filter", "f", "", "filter beacons by substring")
|
||||
@@ -40,7 +41,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
flags.BindFlagCompletions(beaconsCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["kill"] = BeaconIDCompleter(con)
|
||||
(*comp)["interact"] = BeaconIDCompleter(con)
|
||||
})
|
||||
registerBeaconIDFlagCompletion(beaconsCmd, "interact", con)
|
||||
beaconsRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
Short: "Remove a beacon",
|
||||
@@ -81,22 +84,74 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
// BeaconIDCompleter completes beacon IDs
|
||||
func BeaconIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
callback := func(_ carapace.Context) carapace.Action {
|
||||
results := make([]string, 0)
|
||||
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err == nil {
|
||||
for _, b := range beacons.Beacons {
|
||||
link := fmt.Sprintf("[%s <- %s]", b.ActiveC2, b.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", b.Name, b.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", b.Username, b.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, b.ID[:8])
|
||||
results = append(results, desc)
|
||||
}
|
||||
}
|
||||
return carapace.ActionValuesDescribed(results...).Tag("beacons")
|
||||
return carapace.ActionValuesDescribed(beaconCompletionPairs(con)...).Tag("beacons")
|
||||
}
|
||||
|
||||
return carapace.ActionCallback(callback)
|
||||
}
|
||||
|
||||
func registerBeaconIDFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
values := describedValuesToTabs(beaconCompletionPairs(con))
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func beaconCompletionPairs(con *console.SliverClient) []string {
|
||||
results := make([]string, 0)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return results
|
||||
}
|
||||
|
||||
for _, b := range beacons.Beacons {
|
||||
link := fmt.Sprintf("[%s <- %s]", b.ActiveC2, b.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", b.Name, b.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", b.Username, b.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, b.ID[:8], desc)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func describedValuesToTabs(values []string) []string {
|
||||
tabbed := make([]string, 0, len(values)/2)
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
tabbed = append(tabbed, fmt.Sprintf("%s\t%s", values[i], values[i+1]))
|
||||
}
|
||||
return tabbed
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -21,6 +21,7 @@ package c2profiles
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
@@ -42,6 +43,10 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
)
|
||||
|
||||
// C2ProfileCmd list available http profiles
|
||||
func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
profileName, _ := cmd.Flags().GetString("name")
|
||||
@@ -53,7 +58,11 @@ func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
return
|
||||
}
|
||||
if len(httpC2Profiles.Configs) != 1 {
|
||||
profileName = selectC2Profile(httpC2Profiles.Configs)
|
||||
profileName, err = selectC2Profile(httpC2Profiles.Configs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +133,11 @@ func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
return
|
||||
}
|
||||
if len(httpC2Profiles.Configs) != 1 {
|
||||
profileName = selectC2Profile(httpC2Profiles.Configs)
|
||||
profileName, err = selectC2Profile(httpC2Profiles.Configs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +553,7 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
|
||||
con.Println("\n")
|
||||
}
|
||||
|
||||
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {
|
||||
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) (string, error) {
|
||||
c2profile := ""
|
||||
var choices []string
|
||||
for _, c2profile := range c2profiles {
|
||||
@@ -549,7 +562,11 @@ func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {
|
||||
|
||||
_ = forms.Select("Select a c2 profile", choices, &c2profile)
|
||||
|
||||
return c2profile
|
||||
if c2profile == "" {
|
||||
return "", ErrNoSelection
|
||||
}
|
||||
|
||||
return c2profile, nil
|
||||
}
|
||||
|
||||
func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) {
|
||||
|
||||
@@ -25,12 +25,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
flags.Bind("", false, executeCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("token", "T", false, "execute command with current token (Windows only)")
|
||||
f.BoolP("output", "o", false, "capture command output")
|
||||
f.BoolP("output", "o", true, "capture command output")
|
||||
f.BoolP("save", "s", false, "save output to a file")
|
||||
f.BoolP("loot", "X", false, "save output as loot")
|
||||
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
|
||||
f.StringP("stdout", "O", "", "remote path to redirect STDOUT to")
|
||||
f.StringP("stderr", "E", "", "remote path to redirect STDERR to")
|
||||
f.Bool("env-inheritance", true, "inherit environment variables from the current process")
|
||||
f.StringArray("env", nil, "set an environment variable for the child process (key=value)")
|
||||
f.StringP("name", "n", "", "name to assign loot (optional)")
|
||||
f.Uint32P("ppid", "P", 0, "parent process id (optional, Windows only)")
|
||||
f.BoolP("hidden", "H", false, "hide the window of the spawned process (Windows only)")
|
||||
|
||||
@@ -49,6 +49,13 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
saveLoot, _ := cmd.Flags().GetBool("loot")
|
||||
saveOutput, _ := cmd.Flags().GetBool("save")
|
||||
ppid, _ := cmd.Flags().GetUint32("ppid")
|
||||
envInheritance, _ := cmd.Flags().GetBool("env-inheritance")
|
||||
envPairs, _ := cmd.Flags().GetStringArray("env")
|
||||
envVars, err := parseEnvPairs(envPairs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
hostName := getHostname(session, beacon)
|
||||
|
||||
// If the user wants to loot or save the output, we have to capture it regardless of if they specified -o
|
||||
@@ -59,15 +66,19 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
|
||||
var exec *sliverpb.Execute
|
||||
var err error
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.PrintInfof("Execute: %s [%s]\n", cmdPath, strings.Join(args, " "))
|
||||
con.SpinUntil(fmt.Sprintf("Executing %s %s ...", cmdPath, strings.Join(args, " ")), ctrl)
|
||||
if token || hidden || ppid != 0 {
|
||||
if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") {
|
||||
con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS)
|
||||
return
|
||||
}
|
||||
if envInheritance || len(envVars) > 0 {
|
||||
con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n")
|
||||
return
|
||||
}
|
||||
exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: cmdPath,
|
||||
@@ -81,12 +92,14 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
})
|
||||
} else {
|
||||
exec, err = con.Rpc.Execute(context.Background(), &sliverpb.ExecuteReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: cmdPath,
|
||||
Args: args,
|
||||
Output: captureOutput,
|
||||
Stderr: stderr,
|
||||
Stdout: stdout,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: cmdPath,
|
||||
Args: args,
|
||||
Output: captureOutput,
|
||||
Stderr: stderr,
|
||||
Stdout: stdout,
|
||||
EnvInheritance: envInheritance,
|
||||
Env: envVars,
|
||||
})
|
||||
}
|
||||
ctrl <- true
|
||||
@@ -179,6 +192,21 @@ func getHostname(session *clientpb.Session, beacon *clientpb.Beacon) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseEnvPairs(envPairs []string) (map[string]string, error) {
|
||||
if len(envPairs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
envVars := make(map[string]string, len(envPairs))
|
||||
for _, pair := range envPairs {
|
||||
parts := strings.SplitN(pair, "=", 2)
|
||||
if len(parts) != 2 || parts[0] == "" {
|
||||
return nil, fmt.Errorf("invalid --env value %q, expected key=value", pair)
|
||||
}
|
||||
envVars[parts[0]] = parts[1]
|
||||
}
|
||||
return envVars, nil
|
||||
}
|
||||
|
||||
func determineCommandName(command string) string {
|
||||
commandName := strings.ReplaceAll(command, "\\", "/")
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ package exit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -32,26 +31,25 @@ import (
|
||||
|
||||
// ExitCmd - Exit the console.
|
||||
func ExitCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
fmt.Println("Exiting...")
|
||||
if con.IsServer {
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
flushAndExit(con, 1)
|
||||
}
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
flushAndExit(con, 1)
|
||||
}
|
||||
if 0 < len(sessions.Sessions) || 0 < len(beacons.Beacons) {
|
||||
con.Printf("There are %d active sessions and %d active beacons.\n", len(sessions.Sessions), len(beacons.Beacons))
|
||||
confirm := false
|
||||
_ = forms.Confirm("Are you sure you want to exit?", &confirm)
|
||||
forms.Confirm("Are you sure you want to exit?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
flushAndExit(con, 0)
|
||||
}
|
||||
|
||||
// Commands returns the `exit` command.
|
||||
@@ -65,3 +63,12 @@ func Command(con *console.SliverClient) []*cobra.Command {
|
||||
GroupID: constants.GenericHelpGroup,
|
||||
}}
|
||||
}
|
||||
|
||||
func flushAndExit(con *console.SliverClient, code int) {
|
||||
if con != nil {
|
||||
con.FlushOutput()
|
||||
} else {
|
||||
os.Stdout.Sync()
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
@@ -43,8 +42,8 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/carapace-sh/carapace"
|
||||
appConsole "github.com/reeflective/console"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -121,6 +120,7 @@ type extensionArgument struct {
|
||||
Desc string `json:"desc"`
|
||||
Optional bool `json:"optional"`
|
||||
Default interface{} `json:"default,omitempty"`
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ExtCommand) getFileForTarget(targetOS string, targetArch string) (string, error) {
|
||||
@@ -689,94 +689,6 @@ func PrintExtOutput(extName string, commandName string, outputSchema *packages.O
|
||||
}
|
||||
}
|
||||
|
||||
func getExtArgs(_ *cobra.Command, args []string, _ string, ext *ExtCommand) ([]byte, error) {
|
||||
var err error
|
||||
argsBuffer := core.BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
// Parse BOF arguments from grumble
|
||||
missingRequiredArgs := make([]string, 0)
|
||||
|
||||
// If we have an extension that expects a single string, but more than one has been parsed, combine them
|
||||
if len(ext.Arguments) == 1 && strings.Contains(ext.Arguments[0].Type, "string") && len(args) > 0 {
|
||||
// The loop below will only read the first element of args because ext.Arguments is 1
|
||||
args[0] = strings.Join(args, " ")
|
||||
}
|
||||
|
||||
for _, arg := range ext.Arguments {
|
||||
// If we don't have any positional words left to consume,
|
||||
// add the remaining required extension arguments in the
|
||||
// error message.
|
||||
if len(args) == 0 {
|
||||
if !arg.Optional {
|
||||
missingRequiredArgs = append(missingRequiredArgs, "`"+arg.Name+"`")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Else pop a word from the list
|
||||
word := args[0]
|
||||
args = args[1:]
|
||||
|
||||
switch arg.Type {
|
||||
case "integer":
|
||||
fallthrough
|
||||
case "int":
|
||||
val, err := strconv.Atoi(word)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = argsBuffer.AddInt(uint32(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "short":
|
||||
val, err := strconv.Atoi(word)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = argsBuffer.AddShort(uint16(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "string":
|
||||
err = argsBuffer.AddString(word)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "wstring":
|
||||
err = argsBuffer.AddWString(word)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Adding support for filepaths so we can
|
||||
// send binary data like shellcodes to BOFs
|
||||
case "file":
|
||||
data, err := os.ReadFile(word)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = argsBuffer.AddData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if we have missing required arguments
|
||||
if len(missingRequiredArgs) > 0 {
|
||||
return nil, fmt.Errorf("required arguments %s were not provided", strings.Join(missingRequiredArgs, ", "))
|
||||
}
|
||||
|
||||
parsedArgs, err := argsBuffer.GetBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parsedArgs, nil
|
||||
}
|
||||
|
||||
func getBOFArgs(cmd *cobra.Command, args []string, binPath string, ext *ExtCommand) ([]byte, error) {
|
||||
var extensionArgs []byte
|
||||
binData, err := os.ReadFile(binPath)
|
||||
@@ -837,16 +749,27 @@ func checkExtensionArgs(extCmd *ExtCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeExtensionArgCompleter builds the positional arguments completer for the extension.
|
||||
// makeExtensionArgCompleter builds the positional and dash arguments completer for the extension.
|
||||
// It provides completion for:
|
||||
// 1. Positional arguments (before --)
|
||||
// 2. Flag-style arguments after -- (e.g., --process, --shellcode)
|
||||
func makeExtensionArgCompleter(extCmd *ExtCommand, _ *cobra.Command, comps *carapace.Carapace) {
|
||||
var actions []carapace.Action
|
||||
|
||||
for _, arg := range extCmd.Arguments {
|
||||
var action carapace.Action
|
||||
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
action = carapace.ActionFiles().Tag("extension data")
|
||||
// If choices are defined, use them for completion
|
||||
if len(arg.Choices) > 0 {
|
||||
action = carapace.ActionValues(arg.Choices...).Tag("choices")
|
||||
} else {
|
||||
// Fall back to type-based completion
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
action = carapace.ActionFiles().Tag("extension data")
|
||||
default:
|
||||
action = carapace.ActionValues()
|
||||
}
|
||||
}
|
||||
|
||||
usage := fmt.Sprintf("(%s) %s", arg.Type, arg.Desc)
|
||||
@@ -858,6 +781,72 @@ func makeExtensionArgCompleter(extCmd *ExtCommand, _ *cobra.Command, comps *cara
|
||||
}
|
||||
|
||||
comps.PositionalCompletion(actions...)
|
||||
|
||||
// Add dash completion for flag-style arguments after --
|
||||
// The extension arguments are parsed as flags (--name value) by argparser.go
|
||||
if len(extCmd.Arguments) > 0 {
|
||||
// Pre-build the flag completions at registration time (not in a callback)
|
||||
// This ensures the values are captured correctly
|
||||
flagCompletion := makeExtensionFlagNameCompletion(extCmd)
|
||||
|
||||
// Build value completions for each argument type
|
||||
valueCompletions := make(map[string]carapace.Action)
|
||||
for _, arg := range extCmd.Arguments {
|
||||
// If choices are defined, use them for completion
|
||||
if len(arg.Choices) > 0 {
|
||||
valueCompletions[arg.Name] = carapace.ActionValues(arg.Choices...).Tag("choices")
|
||||
} else {
|
||||
// Fall back to type-based completion
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
valueCompletions[arg.Name] = carapace.ActionFiles().Tag("file path")
|
||||
default:
|
||||
valueCompletions[arg.Name] = carapace.ActionValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use DashAnyCompletion with a smart action that determines context
|
||||
comps.DashAnyCompletion(
|
||||
carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
// If typing a flag (starts with -)
|
||||
if strings.HasPrefix(c.Value, "-") {
|
||||
return flagCompletion
|
||||
}
|
||||
|
||||
// If previous arg was a flag, complete its value
|
||||
if len(c.Args) > 0 {
|
||||
lastArg := c.Args[len(c.Args)-1]
|
||||
if strings.HasPrefix(lastArg, "-") {
|
||||
flagName := strings.TrimLeft(lastArg, "-")
|
||||
if action, ok := valueCompletions[flagName]; ok {
|
||||
return action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: show flag names
|
||||
return flagCompletion
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// makeExtensionFlagNameCompletion creates completion for flag names
|
||||
func makeExtensionFlagNameCompletion(extCmd *ExtCommand) carapace.Action {
|
||||
var results []string
|
||||
|
||||
for _, arg := range extCmd.Arguments {
|
||||
flagName := fmt.Sprintf("--%s", arg.Name)
|
||||
desc := arg.Desc
|
||||
if arg.Optional {
|
||||
desc = fmt.Sprintf("[optional] %s", desc)
|
||||
}
|
||||
desc = fmt.Sprintf("(%s) %s", arg.Type, desc)
|
||||
results = append(results, flagName, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("extension arguments")
|
||||
}
|
||||
|
||||
func makeCommandPlatformFilters(extCmd *ExtCommand) map[string]string {
|
||||
|
||||
@@ -32,7 +32,7 @@ func PrintCanaries(con *console.SliverClient, canaries []*clientpb.DNSCanary, bu
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Sliver Name",
|
||||
"Implant Name",
|
||||
"Domain",
|
||||
"Triggered",
|
||||
"First Trigger",
|
||||
|
||||
@@ -357,6 +357,7 @@ func coreImplantFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
|
||||
(*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
|
||||
(*comp)["c2profile"] = HTTPC2Completer(con)
|
||||
})
|
||||
registerImplantTargetFlagCompletions(cmd, con)
|
||||
}
|
||||
|
||||
// coreBeaconFlags binds all flags specific to beacon implants (profiles or compiled).
|
||||
|
||||
@@ -22,7 +22,8 @@ var (
|
||||
// GenerateBeaconCmd - The main command used to generate implant binaries
|
||||
func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if shouldRunGenerateBeaconForm(cmd, con, args) {
|
||||
result, err := forms.GenerateBeaconForm()
|
||||
compiler, _ := compilerTargets(con)
|
||||
result, err := forms.GenerateBeaconForm(compiler)
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
|
||||
@@ -85,12 +85,14 @@ var (
|
||||
|
||||
ErrNoExternalBuilder = errors.New("no external builders are available")
|
||||
ErrNoValidBuilders = errors.New("no valid external builders for target")
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
)
|
||||
|
||||
// GenerateCmd - The main command used to generate implant binaries
|
||||
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if shouldRunGenerateForm(cmd, con, args) {
|
||||
result, err := forms.GenerateForm()
|
||||
compiler, _ := compilerTargets(con)
|
||||
result, err := forms.GenerateForm(compiler)
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
@@ -117,14 +119,15 @@ func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
} else {
|
||||
_, err := externalBuild(name, config, save, con)
|
||||
if err != nil {
|
||||
if err == ErrNoExternalBuilder {
|
||||
switch err {
|
||||
case ErrNoExternalBuilder:
|
||||
con.PrintErrorf("There are no external builders currently connected to the server\n")
|
||||
con.PrintErrorf("See 'builders' command for more information\n")
|
||||
} else if err == ErrNoValidBuilders {
|
||||
case ErrNoValidBuilders:
|
||||
con.PrintErrorf("There are external builders connected to the server, but none can build the target you specified\n")
|
||||
con.PrintErrorf("Invalid target %s\n", fmt.Sprintf("%s:%s/%s", config.Format, config.GOOS, config.GOARCH))
|
||||
con.PrintErrorf("See 'builders' command for more information\n")
|
||||
} else {
|
||||
default:
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
return
|
||||
@@ -1144,6 +1147,11 @@ func selectExternalBuilder(builders []*clientpb.Builder, _ *console.SliverClient
|
||||
}
|
||||
choice := ""
|
||||
_ = forms.Select("Select an external builder:", choices, &choice)
|
||||
|
||||
if choice == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
for _, builder := range builders {
|
||||
if builder.Name == choice {
|
||||
return builder, nil
|
||||
|
||||
@@ -3,11 +3,13 @@ package generate
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GetSliverBinary - Get the binary of an implant based on it's profile.
|
||||
@@ -51,71 +53,112 @@ func GetSliverBinary(profile *clientpb.ImplantProfile, con *console.SliverClient
|
||||
return data, err
|
||||
}
|
||||
|
||||
func compilerTargets(con *console.SliverClient) (*clientpb.Compiler, error) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil, fmt.Errorf("no compiler info")
|
||||
}
|
||||
return con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
}
|
||||
|
||||
func appendUnique(values []string, value string) []string {
|
||||
for _, existing := range values {
|
||||
if existing == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
|
||||
func compilerArchValues(compiler *clientpb.Compiler) []string {
|
||||
results := []string{}
|
||||
|
||||
for _, target := range compiler.Targets {
|
||||
results = appendUnique(results, target.GOARCH)
|
||||
}
|
||||
|
||||
for _, target := range compiler.UnsupportedTargets {
|
||||
results = appendUnique(results, target.GOARCH)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func compilerOSValues(compiler *clientpb.Compiler) []string {
|
||||
results := []string{}
|
||||
|
||||
for _, target := range compiler.Targets {
|
||||
results = appendUnique(results, target.GOOS)
|
||||
}
|
||||
|
||||
for _, target := range compiler.UnsupportedTargets {
|
||||
results = appendUnique(results, target.GOOS)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func filterByPrefix(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func registerTargetFlagCompletion(cmd *cobra.Command, name string, valuesFn func(*clientpb.Compiler) []string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
compiler, err := compilerTargets(con)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
values := valuesFn(compiler)
|
||||
return filterByPrefix(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func registerImplantTargetFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
|
||||
registerTargetFlagCompletion(cmd, "os", compilerOSValues, con)
|
||||
registerTargetFlagCompletion(cmd, "arch", compilerArchValues, con)
|
||||
}
|
||||
|
||||
// FormatCompleter completes builds' architectures.
|
||||
func ArchCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||
compiler, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
compiler, err := compilerTargets(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("No compiler info: %s", err.Error())
|
||||
}
|
||||
|
||||
var results []string
|
||||
|
||||
nextTarget:
|
||||
for _, target := range compiler.Targets {
|
||||
for _, res := range results {
|
||||
if res == target.GOARCH {
|
||||
continue nextTarget
|
||||
}
|
||||
}
|
||||
results = append(results, target.GOARCH)
|
||||
}
|
||||
|
||||
nextUnsupported:
|
||||
for _, target := range compiler.UnsupportedTargets {
|
||||
for _, res := range results {
|
||||
if res == target.GOARCH {
|
||||
continue nextUnsupported
|
||||
}
|
||||
}
|
||||
results = append(results, target.GOARCH)
|
||||
}
|
||||
|
||||
return carapace.ActionValues(results...).Tag("architectures")
|
||||
return carapace.ActionValues(compilerArchValues(compiler)...).Tag("architectures")
|
||||
})
|
||||
}
|
||||
|
||||
// FormatCompleter completes build operating systems.
|
||||
func OSCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||
compiler, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
compiler, err := compilerTargets(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("No compiler info: %s", err.Error())
|
||||
}
|
||||
|
||||
var results []string
|
||||
|
||||
nextTarget:
|
||||
for _, target := range compiler.Targets {
|
||||
for _, res := range results {
|
||||
if res == target.GOOS {
|
||||
continue nextTarget
|
||||
}
|
||||
}
|
||||
results = append(results, target.GOOS)
|
||||
}
|
||||
|
||||
nextUnsupported:
|
||||
for _, target := range compiler.UnsupportedTargets {
|
||||
for _, res := range results {
|
||||
if res == target.GOOS {
|
||||
continue nextUnsupported
|
||||
}
|
||||
}
|
||||
results = append(results, target.GOOS)
|
||||
}
|
||||
|
||||
return carapace.ActionValues(results...).Tag("operating systems")
|
||||
return carapace.ActionValues(compilerOSValues(compiler)...).Tag("operating systems")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ func PrintImplantBuilds(builds *clientpb.ImplantBuilds, filters ImplantBuildFilt
|
||||
config.Format,
|
||||
strings.Join(c2URLs, "\n"),
|
||||
fmt.Sprintf("%v", config.Debug),
|
||||
fmt.Sprintf(config.HTTPC2ConfigName),
|
||||
fmt.Sprintf("%s", config.HTTPC2ConfigName),
|
||||
fmt.Sprintf("%v", builds.ResourceIDs[sliverName].Value),
|
||||
fmt.Sprintf("%v", builds.Staged[sliverName]),
|
||||
})
|
||||
|
||||
@@ -283,6 +283,10 @@ func SelectTrafficEncoder(con *console.SliverClient) string {
|
||||
}
|
||||
sort.Strings(encoderNames)
|
||||
var selectedEncoder string
|
||||
_ = forms.Select("Select a traffic encoder:", encoderNames, &selectedEncoder)
|
||||
err = forms.Select("Select a traffic encoder:", encoderNames, &selectedEncoder)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err)
|
||||
return ""
|
||||
}
|
||||
return selectedEncoder
|
||||
}
|
||||
@@ -68,6 +68,7 @@ var (
|
||||
consts.RunAsStr: runAsHelp,
|
||||
consts.ImpersonateStr: impersonateHelp,
|
||||
consts.RevToSelfStr: revToSelfHelp,
|
||||
consts.ExecuteStr: executeHelp,
|
||||
consts.ExecuteAssemblyStr: executeAssemblyHelp,
|
||||
consts.ExecuteShellcodeStr: executeShellcodeHelp,
|
||||
consts.MigrateStr: migrateHelp,
|
||||
@@ -124,6 +125,22 @@ var (
|
||||
consts.C2ProfileStr + sep + consts.C2GenerateStr: c2GenerateHelp,
|
||||
}
|
||||
|
||||
executeHelp = `[[.Bold]]Command:[[.Normal]] execute <path to executable> [options] -- [args...]
|
||||
|
||||
[[.Bold]]About:[[.Normal]] Execute a program on the remote system with optional arguments, note the subprocess
|
||||
arguments are be separated from the sliver 'execute' arguments with '--'
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
execute /bin/bash --env FOO=1 -- -c env
|
||||
|
||||
[*] Execute: /bin/bash [-c env]
|
||||
[*] Output:
|
||||
FOO=1
|
||||
PWD=/Users/moloch/git/sliver
|
||||
SHLVL=1
|
||||
_=/usr/bin/env
|
||||
`
|
||||
|
||||
jobsHelp = `[[.Bold]]Command:[[.Normal]] jobs <options>
|
||||
[[.Bold]]About:[[.Normal]] Manage jobs/listeners.`
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
@@ -44,6 +45,17 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
|
||||
process, _ := cmd.Flags().GetString("process")
|
||||
config := con.GetActiveSessionConfig()
|
||||
|
||||
/* If the HTTP C2 Config name is not defined, then put in the default value
|
||||
This will have no effect on implants that do not use HTTP C2
|
||||
Also this should be overridden when the build info is pulled from the
|
||||
database, but if there is no build info and we have to create the build
|
||||
from scratch, we need to have something in here.
|
||||
*/
|
||||
if config.HTTPC2ConfigName == "" {
|
||||
config.HTTPC2ConfigName = consts.DefaultC2Profile
|
||||
}
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil("Attempting to create a new sliver session as 'NT AUTHORITY\\SYSTEM'...", ctrl)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["interact"] = SessionIDCompleter(con)
|
||||
(*comp)["kill"] = SessionIDCompleter(con)
|
||||
})
|
||||
registerSessionIDFlagCompletion(sessionsCmd, "interact", con)
|
||||
|
||||
sessionsPruneCmd := &cobra.Command{
|
||||
Use: consts.PruneStr,
|
||||
@@ -63,26 +64,78 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
// SessionIDCompleter completes session IDs.
|
||||
func SessionIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
callback := func(_ carapace.Context) carapace.Action {
|
||||
results := make([]string, 0)
|
||||
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err == nil {
|
||||
for _, s := range sessions.Sessions {
|
||||
link := fmt.Sprintf("[%s <- %s]", s.ActiveC2, s.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", s.Name, s.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", s.Username, s.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, s.ID[:8])
|
||||
results = append(results, desc)
|
||||
}
|
||||
}
|
||||
return carapace.ActionValuesDescribed(results...).Tag("sessions")
|
||||
return carapace.ActionValuesDescribed(sessionCompletionPairs(con)...).Tag("sessions")
|
||||
}
|
||||
|
||||
return carapace.ActionCallback(callback)
|
||||
}
|
||||
|
||||
func registerSessionIDFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
values := describedValuesToTabs(sessionCompletionPairs(con))
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func sessionCompletionPairs(con *console.SliverClient) []string {
|
||||
results := make([]string, 0)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return results
|
||||
}
|
||||
|
||||
for _, s := range sessions.Sessions {
|
||||
link := fmt.Sprintf("[%s <- %s]", s.ActiveC2, s.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", s.Name, s.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", s.Username, s.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, s.ID[:8], desc)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func describedValuesToTabs(values []string) []string {
|
||||
tabbed := make([]string, 0, len(values)/2)
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
tabbed = append(tabbed, fmt.Sprintf("%s\t%s", values[i], values[i+1]))
|
||||
}
|
||||
return tabbed
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// SliverCommands returns all session control commands for the active target.
|
||||
func SliverCommands(con *console.SliverClient) []*cobra.Command {
|
||||
backgroundCmd := &cobra.Command{
|
||||
|
||||
@@ -27,6 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
carapace.Gen(useCmd).PositionalCompletion(BeaconAndSessionIDCompleter(con))
|
||||
registerUseIDCompletion(useCmd, con, true, true)
|
||||
|
||||
useSessionCmd := &cobra.Command{
|
||||
Use: consts.SessionsStr,
|
||||
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
}
|
||||
carapace.Gen(useSessionCmd).PositionalCompletion(SessionIDCompleter(con))
|
||||
registerUseIDCompletion(useSessionCmd, con, true, false)
|
||||
useCmd.AddCommand(useSessionCmd)
|
||||
|
||||
useBeaconCmd := &cobra.Command{
|
||||
@@ -48,6 +50,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
}
|
||||
carapace.Gen(useBeaconCmd).PositionalCompletion(beacons.BeaconIDCompleter(con))
|
||||
registerUseIDCompletion(useBeaconCmd, con, false, true)
|
||||
useCmd.AddCommand(useBeaconCmd)
|
||||
|
||||
return []*cobra.Command{useCmd}
|
||||
|
||||
@@ -39,6 +39,77 @@ import (
|
||||
|
||||
var ErrNoSelection = errors.New("no selection")
|
||||
|
||||
func registerUseIDCompletion(cmd *cobra.Command, con *console.SliverClient, includeSessions, includeBeacons bool) {
|
||||
if cmd == nil || cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
values := useCompletionValues(con, includeSessions, includeBeacons)
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func useCompletionValues(con *console.SliverClient, includeSessions, includeBeacons bool) []string {
|
||||
results := []string{}
|
||||
if con == nil || con.Rpc == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
if includeSessions {
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err == nil {
|
||||
for _, session := range sessions.Sessions {
|
||||
link := fmt.Sprintf("[%s <- %s]", session.ActiveC2, session.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", session.Name, session.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", session.Username, session.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, fmt.Sprintf("%s\t%s", session.ID[:8], desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if includeBeacons {
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err == nil {
|
||||
for _, beacon := range beacons.Beacons {
|
||||
link := fmt.Sprintf("[%s <- %s]", beacon.ActiveC2, beacon.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", beacon.Name, beacon.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", beacon.Username, beacon.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, fmt.Sprintf("%s\t%s", beacon.ID[:8], desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// UseCmd - Change the active session
|
||||
func UseCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var session *clientpb.Session
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package websites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -26,6 +30,41 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
carapace.Gen(websitesCmd).PositionalCompletion(WebsiteNameCompleter(con))
|
||||
|
||||
websitesListCmd := &cobra.Command{
|
||||
Use: consts.ListStr,
|
||||
Short: "List configured websites",
|
||||
Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.ListStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ListWebsites(cmd, con, args)
|
||||
},
|
||||
}
|
||||
websitesCmd.AddCommand(websitesListCmd)
|
||||
|
||||
websitesShowCmd := &cobra.Command{
|
||||
Use: "show [name]",
|
||||
Short: "Show contents of a website",
|
||||
Long: help.GetHelpFor([]string{consts.WebsitesStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ListWebsiteContent(args[0], con)
|
||||
},
|
||||
}
|
||||
carapace.Gen(websitesShowCmd).PositionalCompletion(WebsiteNameCompleter(con))
|
||||
websitesShowCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
ws, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
out := []string{}
|
||||
for _, w := range ws.Websites {
|
||||
if strings.HasPrefix(w.Name, toComplete) {
|
||||
out = append(out, w.Name)
|
||||
}
|
||||
}
|
||||
return out, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
websitesCmd.AddCommand(websitesShowCmd)
|
||||
|
||||
websitesRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
Short: "Remove an entire website and all of its contents",
|
||||
|
||||
@@ -121,9 +121,10 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
|
||||
}
|
||||
|
||||
web.Contents[webpath] = &clientpb.WebContent{
|
||||
Path: webpath,
|
||||
ContentType: contentType,
|
||||
Content: data,
|
||||
Path: webpath,
|
||||
ContentType: contentType,
|
||||
OriginalFile: filepath.Base(contentPath),
|
||||
Content: data,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ package websites
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -58,11 +57,14 @@ func ListWebsites(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
con.PrintInfof("No websites\n")
|
||||
return
|
||||
}
|
||||
con.Println("Websites")
|
||||
con.Println(strings.Repeat("=", len("Websites")))
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{"Name", "Objects"})
|
||||
for _, site := range websites.Websites {
|
||||
con.Printf("%s%s%s - %d page(s)\n", console.Bold, site.Name, console.Normal, len(site.Contents))
|
||||
tw.AppendRow(table.Row{site.Name, len(site.Contents)})
|
||||
}
|
||||
|
||||
con.Println(tw.Render())
|
||||
}
|
||||
|
||||
// ListWebsiteContent - List the static contents of a website.
|
||||
@@ -91,6 +93,8 @@ func PrintWebsite(web *clientpb.Website, con *console.SliverClient) {
|
||||
"Path",
|
||||
"Content-type",
|
||||
"Size",
|
||||
"Original File",
|
||||
"SHA256",
|
||||
})
|
||||
sortedContents := []*clientpb.WebContent{}
|
||||
for _, content := range web.Contents {
|
||||
@@ -104,6 +108,8 @@ func PrintWebsite(web *clientpb.Website, con *console.SliverClient) {
|
||||
content.Path,
|
||||
content.ContentType,
|
||||
content.Size,
|
||||
content.OriginalFile,
|
||||
content.Sha256,
|
||||
})
|
||||
}
|
||||
con.Println(tw.Render())
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/reeflective/console"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (con *SliverClient) allowServerRootCommands(args []string) ([]string, error) {
|
||||
con.applyCommandFallback(args)
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) prepareCompletion(line []rune, cursor int) {
|
||||
if con == nil || con.App == nil {
|
||||
return
|
||||
}
|
||||
|
||||
menu := con.App.ActiveMenu()
|
||||
if menu == nil {
|
||||
return
|
||||
}
|
||||
|
||||
con.setMenuCommandForCompletion(menu, line, cursor)
|
||||
}
|
||||
|
||||
func (con *SliverClient) applyCommandFallback(args []string) {
|
||||
if con == nil || con.App == nil {
|
||||
return
|
||||
}
|
||||
|
||||
con.setMenuCommand(con.App.ActiveMenu(), args)
|
||||
}
|
||||
|
||||
func (con *SliverClient) setMenuCommand(menu *console.Menu, args []string) {
|
||||
if menu == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if menu.Name() != consts.ImplantMenu {
|
||||
if menu.Command == nil && con.serverCmds != nil {
|
||||
menu.Command = con.serverCmds()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if menu.Command == nil {
|
||||
if con.sliverCmds == nil {
|
||||
return
|
||||
}
|
||||
menu.Command = con.sliverCmds()
|
||||
}
|
||||
|
||||
if con.serverCmds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := firstNonFlagArg(args)
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if rootHasCommand(menu.Command, name) {
|
||||
return
|
||||
}
|
||||
|
||||
serverRoot := con.serverCmds()
|
||||
if serverRoot == nil || !rootHasCommand(serverRoot, name) {
|
||||
return
|
||||
}
|
||||
|
||||
menu.Command = serverRoot
|
||||
}
|
||||
|
||||
func (con *SliverClient) setMenuCommandForCompletion(menu *console.Menu, line []rune, cursor int) {
|
||||
if menu == nil || menu.Name() != consts.ImplantMenu {
|
||||
return
|
||||
}
|
||||
|
||||
if menu.Command == nil {
|
||||
if con.sliverCmds == nil {
|
||||
return
|
||||
}
|
||||
menu.Command = con.sliverCmds()
|
||||
}
|
||||
|
||||
if con.serverCmds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
token := firstTokenFromLine(line, cursor)
|
||||
if token == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if rootHasCommandPrefix(menu.Command, token) {
|
||||
return
|
||||
}
|
||||
|
||||
serverRoot := con.serverCmds()
|
||||
if serverRoot == nil || !rootHasCommandPrefix(serverRoot, token) {
|
||||
return
|
||||
}
|
||||
|
||||
menu.Command = serverRoot
|
||||
hideFilteredCommands(menu, menu.Command)
|
||||
}
|
||||
|
||||
func firstNonFlagArg(args []string) string {
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
if arg == "--" {
|
||||
if i+1 < len(args) {
|
||||
return args[i+1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
continue
|
||||
}
|
||||
return arg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstTokenFromLine(line []rune, cursor int) string {
|
||||
if cursor > len(line) {
|
||||
cursor = len(line)
|
||||
}
|
||||
if cursor < 0 {
|
||||
cursor = 0
|
||||
}
|
||||
|
||||
pos := 0
|
||||
for pos < cursor && isSpace(line[pos]) {
|
||||
pos++
|
||||
}
|
||||
start := pos
|
||||
for pos < cursor && !isSpace(line[pos]) {
|
||||
pos++
|
||||
}
|
||||
if start == pos {
|
||||
return ""
|
||||
}
|
||||
return string(line[start:pos])
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
}
|
||||
|
||||
func rootHasCommand(root *cobra.Command, name string) bool {
|
||||
if root == nil || name == "" {
|
||||
return false
|
||||
}
|
||||
for _, cmd := range root.Commands() {
|
||||
if cmd.Name() == name || cmd.HasAlias(name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func rootHasCommandPrefix(root *cobra.Command, prefix string) bool {
|
||||
if root == nil || prefix == "" {
|
||||
return false
|
||||
}
|
||||
for _, cmd := range root.Commands() {
|
||||
if strings.HasPrefix(cmd.Name(), prefix) {
|
||||
return true
|
||||
}
|
||||
for _, alias := range cmd.Aliases {
|
||||
if strings.HasPrefix(alias, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hideFilteredCommands(menu *console.Menu, root *cobra.Command) {
|
||||
if menu == nil || root == nil {
|
||||
return
|
||||
}
|
||||
for _, cmd := range root.Commands() {
|
||||
if cmd.Hidden {
|
||||
continue
|
||||
}
|
||||
if filters := menu.ActiveFiltersFor(cmd); len(filters) > 0 {
|
||||
cmd.Hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
-5
@@ -41,6 +41,7 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/reeflective/console"
|
||||
"github.com/reeflective/readline"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -97,9 +98,14 @@ type SliverClient struct {
|
||||
Settings *assets.ClientSettings
|
||||
IsServer bool
|
||||
IsCLI bool
|
||||
serverCmds console.Commands
|
||||
sliverCmds console.Commands
|
||||
|
||||
jsonHandler slog.Handler
|
||||
printf func(format string, args ...any) (int, error)
|
||||
jsonHandler slog.Handler
|
||||
printf func(format string, args ...any) (int, error)
|
||||
stdoutPipeWriter *os.File
|
||||
stdoutPipeDone chan struct{}
|
||||
stdoutPipeOnce sync.Once
|
||||
}
|
||||
|
||||
// NewConsole creates the sliver client (and console), creating menus and prompts.
|
||||
@@ -159,9 +165,11 @@ func NewConsole(isServer bool) *SliverClient {
|
||||
// Init requires a working RPC connection to the sliver server, and 2 different sets of commands.
|
||||
// If run is true, the console application is started, making this call blocking. Otherwise, commands and
|
||||
// RPC connection are bound to the console (making the console ready to run), but the console does not start.
|
||||
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, sliverCmds console.Commands, run bool) error {
|
||||
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
|
||||
con.Rpc = rpc
|
||||
con.IsCLI = !run
|
||||
con.serverCmds = serverCmds
|
||||
con.sliverCmds = sliverCmds
|
||||
|
||||
// The console application needs to query the terminal for cursor positions
|
||||
// when asynchronously printing logs (that is, when no command is running).
|
||||
@@ -180,6 +188,15 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
|
||||
sliver := con.App.Menu(consts.ImplantMenu)
|
||||
sliver.SetCommands(sliverCmds)
|
||||
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands)
|
||||
if shell := con.App.Shell(); shell != nil && shell.Completer != nil {
|
||||
baseCompleter := shell.Completer
|
||||
shell.Completer = func(line []rune, cursor int) readline.Completions {
|
||||
con.prepareCompletion(line, cursor)
|
||||
return baseCompleter(line, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
// Events
|
||||
go con.startEventLoop()
|
||||
go core.TunnelLoop(rpc)
|
||||
@@ -206,6 +223,13 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
|
||||
con.setupAsciicastRecord(asciicastLog, asciicastStream)
|
||||
}
|
||||
|
||||
if rcScript != "" {
|
||||
originalPrintf := con.printf
|
||||
con.printf = fmt.Printf
|
||||
con.runRCScript(serverCmds, sliverCmds, rcScript)
|
||||
con.printf = originalPrintf
|
||||
}
|
||||
|
||||
if !con.IsCLI {
|
||||
return con.App.Start()
|
||||
}
|
||||
@@ -213,6 +237,88 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
|
||||
return nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) runRCScript(serverCmds, sliverCmds console.Commands, rcScript string) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(rcScript))
|
||||
lineNumber := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNumber++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if err := con.runRCLine(serverCmds, sliverCmds, line); err != nil {
|
||||
con.PrintErrorf("rc line %d error: %s", lineNumber, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
con.PrintErrorf("rc script error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line string) error {
|
||||
args, err := shellquote.Split(line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse error: %w", err)
|
||||
}
|
||||
|
||||
if con.serverCmds == nil {
|
||||
con.serverCmds = serverCmds
|
||||
}
|
||||
if con.sliverCmds == nil {
|
||||
con.sliverCmds = sliverCmds
|
||||
}
|
||||
|
||||
menu := con.App.ActiveMenu()
|
||||
if menu != nil {
|
||||
// Reset per line to avoid stale roots when rc scripts switch menus.
|
||||
menu.Command = nil
|
||||
}
|
||||
|
||||
for _, hook := range con.App.PreCmdRunLineHooks {
|
||||
args, err = hook(args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pre-run line error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
menu = con.App.ActiveMenu()
|
||||
if menu == nil {
|
||||
return fmt.Errorf("no active menu")
|
||||
}
|
||||
if menu.Command == nil {
|
||||
con.setMenuCommand(menu, args)
|
||||
}
|
||||
if menu.Command == nil {
|
||||
return fmt.Errorf("no commands available")
|
||||
}
|
||||
|
||||
target, _, _ := menu.Command.Find(args)
|
||||
if err := menu.CheckIsAvailable(target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, hook := range con.App.PreCmdRunHooks {
|
||||
if err := hook(); err != nil {
|
||||
return fmt.Errorf("pre-run error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
menu.SetArgs(args)
|
||||
menu.SetContext(context.Background())
|
||||
|
||||
if err := menu.Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) startEventLoop() {
|
||||
eventStream, err := con.Rpc.Events(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
@@ -341,11 +447,12 @@ func (con *SliverClient) triggerReactions(event *clientpb.Event) {
|
||||
con.ActiveTarget.Set(currentActiveSession, currentActiveBeacon)
|
||||
}()
|
||||
|
||||
if event.EventType == consts.SessionOpenedEvent {
|
||||
switch event.EventType {
|
||||
case consts.SessionOpenedEvent:
|
||||
con.ActiveTarget.Set(nil, nil)
|
||||
|
||||
con.ActiveTarget.Set(event.Session, nil)
|
||||
} else if event.EventType == consts.BeaconRegisteredEvent {
|
||||
case consts.BeaconRegisteredEvent:
|
||||
con.ActiveTarget.Set(nil, nil)
|
||||
|
||||
beacon := &clientpb.Beacon{}
|
||||
@@ -609,6 +716,7 @@ func (c *SliverClient) exitConsole(_ *console.Console) {
|
||||
answer := strings.TrimSpace(text)
|
||||
|
||||
if (answer == "Y") || (answer == "y") {
|
||||
c.FlushOutput()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
+27
-1
@@ -124,7 +124,14 @@ func (con *SliverClient) setupAsciicastRecord(logFile *os.File, server io.Writer
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
|
||||
go io.Copy(mw, r)
|
||||
done := make(chan struct{})
|
||||
con.stdoutPipeWriter = w
|
||||
con.stdoutPipeDone = done
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(mw, r)
|
||||
close(done)
|
||||
}()
|
||||
}
|
||||
|
||||
func getConsoleLogFile() *os.File {
|
||||
@@ -149,6 +156,25 @@ func getConsoleAsciicastFile() *os.File {
|
||||
return logFile
|
||||
}
|
||||
|
||||
// FlushOutput drains any piped stdout before exiting.
|
||||
func (con *SliverClient) FlushOutput() {
|
||||
if con.stdoutPipeWriter == nil {
|
||||
_ = os.Stdout.Sync()
|
||||
return
|
||||
}
|
||||
|
||||
con.stdoutPipeOnce.Do(func() {
|
||||
_ = con.stdoutPipeWriter.Close()
|
||||
})
|
||||
|
||||
if con.stdoutPipeDone != nil {
|
||||
select {
|
||||
case <-con.stdoutPipeDone:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// -------------------------- [ Logging ] -----------------------------
|
||||
//
|
||||
|
||||
+30
-34
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
@@ -23,62 +24,57 @@ type GenerateFormResult struct {
|
||||
}
|
||||
|
||||
// GenerateForm prompts for core generate flags and returns the collected values.
|
||||
func GenerateForm() (*GenerateFormResult, error) {
|
||||
func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
|
||||
result := &GenerateFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
}
|
||||
commonPlatformsOnly := true
|
||||
lastCommonPlatformsOnly := commonPlatformsOnly
|
||||
compilerTargets := compilerTargetList(compiler)
|
||||
targetBindings := &targetOptionBindings{
|
||||
Common: &commonPlatformsOnly,
|
||||
OS: &result.OS,
|
||||
Arch: &result.Arch,
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("Common Platforms Only").
|
||||
Value(&commonPlatformsOnly),
|
||||
huh.NewSelect[string]().
|
||||
Title("Target operating system").
|
||||
Options(
|
||||
huh.NewOption("Windows", "windows"),
|
||||
huh.NewOption("Linux", "linux"),
|
||||
huh.NewOption("macOS", "darwin"),
|
||||
).
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
|
||||
if commonPlatformsOnly || len(compilerTargets) == 0 {
|
||||
return commonOSOptions()
|
||||
}
|
||||
return osOptionsFromTargets(compilerTargets)
|
||||
}, targetBindings).
|
||||
Value(&result.OS),
|
||||
huh.NewSelect[string]().
|
||||
Title("CPU architecture").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
switch result.OS {
|
||||
case "darwin":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("arm64", "arm64"),
|
||||
}
|
||||
case "linux":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
default:
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
|
||||
if commonPlatformsOnly || len(compilerTargets) == 0 {
|
||||
return commonArchOptions(result.OS)
|
||||
}
|
||||
}, &result.OS).
|
||||
return archOptionsFromTargets(compilerTargets, result.OS)
|
||||
}, targetBindings).
|
||||
Height(3).
|
||||
Value(&result.Arch),
|
||||
huh.NewSelect[string]().
|
||||
Title("Output format").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("Executable", "exe"),
|
||||
huh.NewOption("Shared library", "shared"),
|
||||
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
|
||||
if commonPlatformsOnly || len(compilerTargets) == 0 {
|
||||
return commonFormatOptions(result.OS)
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Service", "service"),
|
||||
huh.NewOption("Shellcode", "shellcode"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
return formatOptionsFromTargets(compilerTargets, result.OS, result.Arch)
|
||||
}, targetBindings).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
@@ -26,62 +27,57 @@ type GenerateBeaconFormResult struct {
|
||||
}
|
||||
|
||||
// GenerateBeaconForm prompts for generate beacon flags and returns the collected values.
|
||||
func GenerateBeaconForm() (*GenerateBeaconFormResult, error) {
|
||||
func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult, error) {
|
||||
result := &GenerateBeaconFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
}
|
||||
commonPlatformsOnly := true
|
||||
lastCommonPlatformsOnly := commonPlatformsOnly
|
||||
compilerTargets := compilerTargetList(compiler)
|
||||
targetBindings := &targetOptionBindings{
|
||||
Common: &commonPlatformsOnly,
|
||||
OS: &result.OS,
|
||||
Arch: &result.Arch,
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("Common Platforms Only").
|
||||
Value(&commonPlatformsOnly),
|
||||
huh.NewSelect[string]().
|
||||
Title("Target operating system").
|
||||
Options(
|
||||
huh.NewOption("Windows", "windows"),
|
||||
huh.NewOption("Linux", "linux"),
|
||||
huh.NewOption("macOS", "darwin"),
|
||||
).
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
|
||||
if commonPlatformsOnly || len(compilerTargets) == 0 {
|
||||
return commonOSOptions()
|
||||
}
|
||||
return osOptionsFromTargets(compilerTargets)
|
||||
}, targetBindings).
|
||||
Value(&result.OS),
|
||||
huh.NewSelect[string]().
|
||||
Title("CPU architecture").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
switch result.OS {
|
||||
case "darwin":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("arm64", "arm64"),
|
||||
}
|
||||
case "linux":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
default:
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
|
||||
if commonPlatformsOnly || len(compilerTargets) == 0 {
|
||||
return commonArchOptions(result.OS)
|
||||
}
|
||||
}, &result.OS).
|
||||
return archOptionsFromTargets(compilerTargets, result.OS)
|
||||
}, targetBindings).
|
||||
Height(3).
|
||||
Value(&result.Arch),
|
||||
huh.NewSelect[string]().
|
||||
Title("Output format").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("Executable", "exe"),
|
||||
huh.NewOption("Shared library", "shared"),
|
||||
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
|
||||
if commonPlatformsOnly || len(compilerTargets) == 0 {
|
||||
return commonFormatOptions(result.OS)
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Service", "service"),
|
||||
huh.NewOption("Shellcode", "shellcode"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
return formatOptionsFromTargets(compilerTargets, result.OS, result.Arch)
|
||||
}, targetBindings).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
type formatOption struct {
|
||||
format clientpb.OutputFormat
|
||||
label string
|
||||
value string
|
||||
}
|
||||
|
||||
type targetOptionBindings struct {
|
||||
Common *bool
|
||||
OS *string
|
||||
Arch *string
|
||||
}
|
||||
|
||||
var formatOptionOrder = []formatOption{
|
||||
{format: clientpb.OutputFormat_EXECUTABLE, label: "Executable", value: "exe"},
|
||||
{format: clientpb.OutputFormat_SHARED_LIB, label: "Shared library", value: "shared"},
|
||||
{format: clientpb.OutputFormat_SERVICE, label: "Service", value: "service"},
|
||||
{format: clientpb.OutputFormat_SHELLCODE, label: "Shellcode", value: "shellcode"},
|
||||
}
|
||||
|
||||
func commonOSOptions() []huh.Option[string] {
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("Windows", "windows"),
|
||||
huh.NewOption("Linux", "linux"),
|
||||
huh.NewOption("macOS", "darwin"),
|
||||
}
|
||||
}
|
||||
|
||||
func commonArchOptions(goos string) []huh.Option[string] {
|
||||
switch goos {
|
||||
case "darwin":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("arm64", "arm64"),
|
||||
}
|
||||
case "linux":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
default:
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func commonFormatOptions(goos string) []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("Executable", "exe"),
|
||||
huh.NewOption("Shared library", "shared"),
|
||||
}
|
||||
if goos == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Service", "service"),
|
||||
huh.NewOption("Shellcode", "shellcode"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func firstOptionValue(options []huh.Option[string]) (string, bool) {
|
||||
if len(options) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return options[0].Value, true
|
||||
}
|
||||
|
||||
func maybeResetTargets(commonPlatformsOnly bool, lastCommonPlatformsOnly *bool, compilerTargets []*clientpb.CompilerTarget, goos, goarch, format *string) {
|
||||
if lastCommonPlatformsOnly == nil || goos == nil || goarch == nil || format == nil {
|
||||
return
|
||||
}
|
||||
if commonPlatformsOnly == *lastCommonPlatformsOnly {
|
||||
return
|
||||
}
|
||||
*lastCommonPlatformsOnly = commonPlatformsOnly
|
||||
if commonPlatformsOnly {
|
||||
return
|
||||
}
|
||||
|
||||
osOptions := commonOSOptions()
|
||||
if len(compilerTargets) > 0 {
|
||||
osOptions = osOptionsFromTargets(compilerTargets)
|
||||
if len(osOptions) == 0 {
|
||||
osOptions = commonOSOptions()
|
||||
}
|
||||
}
|
||||
if value, ok := firstOptionValue(osOptions); ok {
|
||||
*goos = value
|
||||
}
|
||||
|
||||
archOptions := commonArchOptions(*goos)
|
||||
if len(compilerTargets) > 0 {
|
||||
archOptions = archOptionsFromTargets(compilerTargets, *goos)
|
||||
if len(archOptions) == 0 {
|
||||
archOptions = commonArchOptions(*goos)
|
||||
}
|
||||
}
|
||||
if value, ok := firstOptionValue(archOptions); ok {
|
||||
*goarch = value
|
||||
}
|
||||
|
||||
formatOptions := commonFormatOptions(*goos)
|
||||
if len(compilerTargets) > 0 {
|
||||
formatOptions = formatOptionsFromTargets(compilerTargets, *goos, *goarch)
|
||||
if len(formatOptions) == 0 {
|
||||
formatOptions = commonFormatOptions(*goos)
|
||||
}
|
||||
}
|
||||
if value, ok := firstOptionValue(formatOptions); ok {
|
||||
*format = value
|
||||
}
|
||||
}
|
||||
|
||||
func compilerTargetList(compiler *clientpb.Compiler) []*clientpb.CompilerTarget {
|
||||
if compiler == nil {
|
||||
return nil
|
||||
}
|
||||
targets := make([]*clientpb.CompilerTarget, 0, len(compiler.Targets)+len(compiler.UnsupportedTargets))
|
||||
targets = append(targets, compiler.Targets...)
|
||||
targets = append(targets, compiler.UnsupportedTargets...)
|
||||
return targets
|
||||
}
|
||||
|
||||
func osOptionsFromTargets(targets []*clientpb.CompilerTarget) []huh.Option[string] {
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, target := range targets {
|
||||
if target.GOOS == "" {
|
||||
continue
|
||||
}
|
||||
seen[target.GOOS] = struct{}{}
|
||||
}
|
||||
values := make([]string, 0, len(seen))
|
||||
for value := range seen {
|
||||
values = append(values, value)
|
||||
}
|
||||
sort.Strings(values)
|
||||
|
||||
options := make([]huh.Option[string], 0, len(values))
|
||||
for _, value := range values {
|
||||
options = append(options, huh.NewOption(osLabel(value), value))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func archOptionsFromTargets(targets []*clientpb.CompilerTarget, goos string) []huh.Option[string] {
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, target := range targets {
|
||||
if goos != "" && target.GOOS != goos {
|
||||
continue
|
||||
}
|
||||
if target.GOARCH == "" {
|
||||
continue
|
||||
}
|
||||
seen[target.GOARCH] = struct{}{}
|
||||
}
|
||||
values := make([]string, 0, len(seen))
|
||||
for value := range seen {
|
||||
values = append(values, value)
|
||||
}
|
||||
sort.Strings(values)
|
||||
|
||||
options := make([]huh.Option[string], 0, len(values))
|
||||
for _, value := range values {
|
||||
options = append(options, huh.NewOption(value, value))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func formatOptionsFromTargets(targets []*clientpb.CompilerTarget, goos, goarch string) []huh.Option[string] {
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
available := map[clientpb.OutputFormat]struct{}{}
|
||||
for _, target := range targets {
|
||||
if goos != "" && target.GOOS != goos {
|
||||
continue
|
||||
}
|
||||
if goarch != "" && target.GOARCH != goarch {
|
||||
continue
|
||||
}
|
||||
available[target.Format] = struct{}{}
|
||||
}
|
||||
|
||||
options := make([]huh.Option[string], 0, len(available))
|
||||
for _, option := range formatOptionOrder {
|
||||
if _, ok := available[option.format]; ok {
|
||||
options = append(options, huh.NewOption(option.label, option.value))
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func osLabel(value string) string {
|
||||
switch value {
|
||||
case "windows":
|
||||
return "Windows"
|
||||
case "linux":
|
||||
return "Linux"
|
||||
case "darwin":
|
||||
return "macOS"
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
+23
-3
@@ -5,10 +5,20 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const defaultSelectHeight = 10
|
||||
|
||||
func getTerminalWidth() int {
|
||||
// Try to get actual terminal width
|
||||
if width, _, err := term.GetSize(1); err == nil && width > 0 {
|
||||
return width
|
||||
}
|
||||
// Fall back to a reasonable default
|
||||
return 200
|
||||
}
|
||||
|
||||
// Confirm prompts for a yes/no answer.
|
||||
func Confirm(title string, value *bool) error {
|
||||
if value == nil {
|
||||
@@ -86,7 +96,7 @@ func MultiSelect(title string, options []string, value *[]string) error {
|
||||
Height(listHeight(len(options))).
|
||||
Value(value)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithWidth(getTerminalWidth())
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
@@ -98,6 +108,8 @@ func selectPrompt(title string, options []string, value *string, required bool)
|
||||
return errors.New("select options are required")
|
||||
}
|
||||
|
||||
// Save the original value in case the form is cancelled
|
||||
originalValue := *value
|
||||
ensureSelectedValue(options, value)
|
||||
|
||||
field := huh.NewSelect[string]().
|
||||
@@ -115,8 +127,16 @@ func selectPrompt(title string, options []string, value *string, required bool)
|
||||
})
|
||||
}
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
return form.Run()
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithWidth(getTerminalWidth())
|
||||
err := form.Run()
|
||||
|
||||
// On error restore the originalValue and return err
|
||||
if err != nil {
|
||||
*value = originalValue
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureSelectedValue(options []string, value *string) {
|
||||
|
||||
@@ -22,7 +22,7 @@ export type TutorialCardCardProps = {
|
||||
|
||||
export default function TutorialCard(props: TutorialCardCardProps) {
|
||||
return (
|
||||
<Card isFooterBlurred>
|
||||
<Card isFooterBlurred className="relative z-0">
|
||||
<CardHeader className="absolute z-10 top-1 flex-col items-end">
|
||||
<p className="text-md text-white/70 uppercase font-bold">
|
||||
{props.name}
|
||||
@@ -41,7 +41,7 @@ export default function TutorialCard(props: TutorialCardCardProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardFooter className="absolute bg-black/40 bottom-0 z-100 border-t-1 border-default-600 dark:border-default-100">
|
||||
<CardFooter className="absolute bottom-0 z-10 bg-black/40 border-t-1 border-default-600 dark:border-default-100">
|
||||
<div className="flex flex-row w-full items-center">
|
||||
<p
|
||||
className={
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
__IMPORTANT:__ MCP (Model Context Protocol) support is _experimental_ and NOT ALL FUNCTIONALITY IS SUPPORTED YET. Pull requests and testing feedback are welcome!
|
||||
|
||||
There are two main ways to connect an AI model to sliver using MCP: locally over stdio, or over a network using HTTP/SSE transports. The `sliver-client` supports both stdio and http/sse, but the `sliver-server` binary only supports HTTP/SSE. We recommend using the `sliver-client` and [Multiplayer Mode](/docs?name=Multi-player+Mode) to connect to a remote server with the stdio transport.
|
||||
|
||||
|
||||
## MCP Over STDIO
|
||||
|
||||
Example configurations for various AIs, you'll need to configure [Multiplayer Mode](/docs?name=Multi-player+Mode) first:
|
||||
|
||||
### OpenAI Codex
|
||||
|
||||
```
|
||||
[mcp_servers.sliver]
|
||||
args = ["mcp", "--config", "/path/to/multiplayer.cfg"]
|
||||
command = "/path/to/sliver-client"
|
||||
```
|
||||
|
||||
### Anthropic Claude Code
|
||||
|
||||
```
|
||||
claude mcp add sliver -- /path/to/sliver-client mcp --config /path/to/multiplayer.cfg
|
||||
```
|
||||
|
||||
## MCP Over HTTP/SSE
|
||||
|
||||
The MCP HTTP/SSE server can be started from either the `sliver-client` or `sliver-server`, however _where you run the command is where the server will start_ i.e., starting the HTTP/SSE MCP using `sliver-client` will turn that `sliver-client` binary into the MCP server (connected to whatever Sliver server you're currently connected to). Running the command from the `sliver-server` console will start a HTTP/SSE MCP server locally bound on the server.
|
||||
|
||||
```
|
||||
.------..------..------..------..------..------.
|
||||
|S.--. ||L.--. ||I.--. ||V.--. ||E.--. ||R.--. |
|
||||
| :/\: || :/\: || (\/) || :(): || (\/) || :(): |
|
||||
| :\/: || (__) || :\/: || ()() || :\/: || ()() |
|
||||
| '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R|
|
||||
`------'`------'`------'`------'`------'`------'
|
||||
|
||||
All hackers gain skulk
|
||||
[*] Server v1.6.4 - 4d0d46afd6e1cbcaafde92687144cb28694a68ce
|
||||
[*] Welcome to the sliver shell, please type 'help' for options
|
||||
sliver > mcp
|
||||
|
||||
Status: stopped
|
||||
Transport: sse
|
||||
Listen: 127.0.0.1:8080
|
||||
Endpoint: http://127.0.0.1:8080/sse
|
||||
|
||||
sliver > mcp start --transport http
|
||||
|
||||
[*] Starting MCP server (http) on 127.0.0.1:8080
|
||||
[*] Endpoint: http://127.0.0.1:8080/mcp
|
||||
```
|
||||
|
||||
You can use `mcp stop` to stop the server, see `mcp start --help` for additional options when starting the MCP server. Authentication is not yet supported for MCP over HTTP/SSE.
|
||||
@@ -1,5 +1,3 @@
|
||||
# This course is intended for the 1.6 version of Sliver, which is not yet published
|
||||
|
||||
Sliver implants support two types of connections, sessions and beacons.
|
||||
|
||||
Sessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# This course is intended for the 1.6 version of Sliver, which is not yet published
|
||||
|
||||
When generating implants Sliver uses a C2Profile configuration to define how implant callbacks will look over HTTP/s. This allowed operators to control headers, cookies proxies, URL paths and so on. When the implant is generated the sliver server will then select portions of that configuration to generate the implant C2Profile.
|
||||
|
||||
For example if configured to use `/admin` and `/demo` as callback urls, it might use one, the other or both depending on your configuration allowing two implants using the same configuration to still seem slightly different from a network traffic perspective.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# This course is intended for the 1.6 version of Sliver, which is not yet published
|
||||
|
||||
When using Sliver during a live engagement, you’re going to need to use custom stagers, which are essentially a first binary or commandline that will retrieve and/or load Sliver into memory on your target system. Sliver can generate shellcode for your stager to execute by using the `profiles` command.
|
||||
|
||||
For this exercise, we will create a new beacon profile and prepare to stage it.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# This course is intended for the 1.6 version of Sliver, which is not yet published
|
||||
|
||||
Pivots allow routing implant traffic through other implants. This can be useful in environments that don’t have any outbound access, but are reachable from other parts of the network that you have access to.
|
||||
|
||||
Sliver supports two types of pivots: TCP, which can be used on all operating systems, and named pipes, which are Windows-only.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# This course is intended for the 1.6 version of Sliver, which is not yet published
|
||||
|
||||
Reactions are a basic way to automate tasks in the sliver console, they allow you to specify sliver commands to run on a list of events.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# This course is intended for the 1.6 version of Sliver, which is not yet published
|
||||
|
||||
The Sliver armory is used to install and maintain third party extensions and aliases within sliver. The full list of available extensions can be found at https://github.com/sliverarmory/armory, keep in mind this is community maintained so not all modules are necessarily up to date.
|
||||
|
||||
You can download all configured extensions/aliases using the armory command.
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"tutorials":[{"name":"1 - Getting Started","content":"# This course is intended for the 1.6 version of Sliver, which is not yet published\n\n`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"# This course is intended for the 1.6 version of Sliver, which is not yet published\n\nSliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n Line truncated
|
||||
{"tutorials":[{"name":"1 - Getting Started","content":"# This course is intended for the 1.6 version of Sliver, which is not yet published\n\n`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-Line truncated
|
||||
@@ -1,16 +1,18 @@
|
||||
module github.com/bishopfox/sliver
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.5
|
||||
|
||||
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
|
||||
|
||||
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989
|
||||
|
||||
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260128035037-680800942770
|
||||
|
||||
require (
|
||||
filippo.io/age v1.2.1
|
||||
filippo.io/age v1.3.1
|
||||
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
|
||||
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
|
||||
github.com/Binject/go-donut v0.0.0-20220908180326-fcdcc35d591c
|
||||
github.com/Binject/go-donut v0.0.0-20210701074227-67a31e2d883e
|
||||
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0
|
||||
github.com/Ne0nd0g/go-clr v1.0.3
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
@@ -33,48 +35,51 @@ require (
|
||||
github.com/lesnuages/go-winio v0.4.19
|
||||
github.com/lesnuages/snitch v0.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/miekg/dns v1.1.70
|
||||
github.com/moloch--/asciicast v0.1.1
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e
|
||||
github.com/moloch--/sgn v0.0.4
|
||||
github.com/ncruces/go-sqlite3 v0.29.1
|
||||
github.com/ncruces/go-sqlite3 v0.30.4
|
||||
github.com/reeflective/console v0.1.25
|
||||
github.com/reeflective/readline v1.1.3
|
||||
github.com/reeflective/readline v1.1.4
|
||||
github.com/rsteube/carapace v0.50.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tetratelabs/wazero v1.9.0
|
||||
github.com/tetratelabs/wazero v1.11.0
|
||||
github.com/things-go/go-socks5 v0.1.0
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/xlab/treeprint v1.2.0
|
||||
github.com/yiya1989/sshkrb5 v0.0.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9
|
||||
golang.org/x/mod v0.30.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
|
||||
modernc.org/sqlite v1.39.0
|
||||
tailscale.com v1.90.6
|
||||
tailscale.com v1.92.5
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/creachadair/msync v0.7.1 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
@@ -96,8 +101,8 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/carapace-sh/carapace v1.9.0 // indirect
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0 // indirect
|
||||
github.com/carapace-sh/carapace v1.11.0
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||
@@ -142,7 +147,7 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mark3labs/mcp-go v0.43.2
|
||||
@@ -182,7 +187,7 @@ require (
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
|
||||
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
|
||||
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
|
||||
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
|
||||
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
|
||||
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
|
||||
filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb h1:c1Bb1n1wr2xgTHkNVxkUHBshff33m+IXgY6lE6oYttg=
|
||||
@@ -17,8 +19,8 @@ github.com/Binject/debug v0.0.0-20210225042342-c9b8b45728d2/go.mod h1:QzgxDLY/qd
|
||||
github.com/Binject/debug v0.0.0-20210312092933-6277045c2fdf/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
|
||||
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a h1:4c0nc0krv8eh7gD809n+swLaCuFyHpxdrxwx0ZmHvBw=
|
||||
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
|
||||
github.com/Binject/go-donut v0.0.0-20220908180326-fcdcc35d591c h1:cFMQKxryHZ3Sddca8nqJ1RDjaUTW/+79lRKs+7508nk=
|
||||
github.com/Binject/go-donut v0.0.0-20220908180326-fcdcc35d591c/go.mod h1:dc3mUnr4KTKcFKVq7BVbHGF0xAHrIyooQ+VTO7/bIZw=
|
||||
github.com/Binject/go-donut v0.0.0-20210701074227-67a31e2d883e h1:ytVmxGQuS7ELO/WpvH6iuY1hVcJ6iOTw3VLOOIFlo8o=
|
||||
github.com/Binject/go-donut v0.0.0-20210701074227-67a31e2d883e/go.mod h1:dc3mUnr4KTKcFKVq7BVbHGF0xAHrIyooQ+VTO7/bIZw=
|
||||
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 h1:T8z8Wz/fqaPPANF8Unv4LjWLoa0TAvsjYAsRpvSkOqs=
|
||||
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563/go.mod h1:HMbzsKPz1sF7H4Hmeovh+d2PH2iKPYpAB4XMOVz8wmM=
|
||||
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0 h1:1sdnMvqxxlMXsu8ku/Xrh6gi3V40ykksst5fd0lfWnc=
|
||||
@@ -89,10 +91,10 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/carapace-sh/carapace v1.9.0 h1:IuEP6YeeK4fhrH7MoOyL0I3GMhvROerkw6YGxkx8Fbw=
|
||||
github.com/carapace-sh/carapace v1.9.0/go.mod h1:Zs3DpsawpFm+8fEfRVYE9EzMBYH8u7S3Aw9yAAKX0JM=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0 h1:58QSJTUxghpa/rEn6+Z9UM/Hy0ipVHDUn9S1mp4i+ZE=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
|
||||
github.com/carapace-sh/carapace v1.11.0 h1:dTUFXvIrUTLd9BGLsfDt4wCgEyPl8bJeZOFDmN7fIfo=
|
||||
github.com/carapace-sh/carapace v1.11.0/go.mod h1:NVkuv1s8wyFvu7d98rdgYwqGKMB6GO1MEUZp5RcWri8=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 h1:ccmNeetAYZOk4IcV36youFDsXusT9uCNW2Njkw+QS+Q=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -146,6 +148,8 @@ github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNs
|
||||
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creachadair/msync v0.7.1 h1:SeZmuEBXQPe5GqV/C94ER7QIZPwtvFbeQiykzt/7uho=
|
||||
github.com/creachadair/msync v0.7.1/go.mod h1:8CcFlLsSujfHE5wWm19uUBLHIPDAUr6LXDwneVMO008=
|
||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
@@ -338,20 +342,22 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/moloch--/asciicast v0.1.1 h1:btwwniCK9br3ys33XZ71IG/M3ZujSSCcBzsH/sieiXg=
|
||||
github.com/moloch--/asciicast v0.1.1/go.mod h1:OckO16UDLgxVLclrCnbocL1ix15Br/8Xv/caBoYq98o=
|
||||
github.com/moloch--/console v0.0.0-20260128035037-680800942770 h1:JtfE7Od0O5ISD4b9r9AX7FTlAf8sT6O4qr5HV6Ey5U0=
|
||||
github.com/moloch--/console v0.0.0-20260128035037-680800942770/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
|
||||
github.com/moloch--/go-keystone v0.0.2 h1:xThbYs45CSzoDUz5kBZxAyTPIpK3t5M7NB1x+NTTRL8=
|
||||
github.com/moloch--/go-keystone v0.0.2/go.mod h1:kpGRJpw9b7Eq3MluHrba+GPKZhSyDrvhLULSwH5nGGM=
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e h1:IkFCPlAa0iTiLxck+NqAwBx8JDlnHYm4orOQBbs4BDQ=
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e/go.mod h1:eYeI6cQ5YHhHt9i0BBW0zc1DaQnb4ZMXsSPuEV/V5Og=
|
||||
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d h1:9RbKockJl6ZdgoEh2QqU3An6Ij/NWZtRWS3J6yG0+qo=
|
||||
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989 h1:g0EfxAWzDGYarN0RAPibsjz2M/f0pAqYNiEpCPWRDok=
|
||||
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/sgn v0.0.4 h1:90IC5rx3TmPEfrqPnENB0yyiqcbcNk9LqezXx7uruF0=
|
||||
github.com/moloch--/sgn v0.0.4/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
@@ -362,8 +368,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-sqlite3 v0.29.1 h1:NIi8AISWBToRHyoz01FXiTNvU147Tqdibgj2tFzJCqM=
|
||||
github.com/ncruces/go-sqlite3 v0.29.1/go.mod h1:PpccBNNhvjwUOwDQEn2gXQPFPTWdlromj0+fSkd5KSg=
|
||||
github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA=
|
||||
github.com/ncruces/go-sqlite3 v0.30.4/go.mod h1:7WR20VSC5IZusKhUdiR9y1NsUqnZgqIYCmKKoMEYg68=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
@@ -377,6 +383,8 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
@@ -387,12 +395,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
||||
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
|
||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/reeflective/console v0.1.25 h1:do/r6rcKH6pCQDEzh5PhoVFcqHkqcjd0N6tNjpmYWWQ=
|
||||
github.com/reeflective/console v0.1.25/go.mod h1:It/IqEjo0PowaBuxBS9pEi5EYUgmtvQd0ytMtlS77kk=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -413,11 +419,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -454,8 +458,8 @@ github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
|
||||
github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
|
||||
github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg=
|
||||
@@ -498,6 +502,7 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
@@ -521,8 +526,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -531,8 +536,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
@@ -561,8 +566,8 @@ golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -581,8 +586,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -611,8 +616,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -645,8 +650,8 @@ gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgI
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
|
||||
honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
|
||||
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 h1:5SXjd4ET5dYijLaf0O3aOenC0Z4ZafIWSpjUzsQaNho=
|
||||
honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
@@ -679,5 +684,5 @@ mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
|
||||
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
tailscale.com v1.90.6 h1:EhYPiZP/xcLeinikaLA0kn4CQT3+z9SZ13IB/kzWhd4=
|
||||
tailscale.com v1.90.6/go.mod h1:+9EX6pOGCNa6pxCVRhhlJLy/qnkDzOplFYpeZyYlCT0=
|
||||
tailscale.com v1.92.5 h1:h88HqtGk8jd9umF7l6m/8+QK69dY9IVMIlkNK9XYpvQ=
|
||||
tailscale.com v1.92.5/go.mod h1:jzTfKDd6XNNSNoX+Q9INIMpMU5rfZ9g8ojcAoMKi5w0=
|
||||
@@ -27,10 +27,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -164,10 +166,14 @@ func dirListHandler(data []byte, resp RPCResponse) {
|
||||
dirList.Exists = true
|
||||
} else {
|
||||
dirList.Exists = false
|
||||
dirList.Response = &commonpb.Response{Err: err.Error()}
|
||||
}
|
||||
dirList.Files = []*sliverpb.FileInfo{}
|
||||
rootDirInfo, err := rootDirEntry.Info()
|
||||
if err == nil && filter == "" {
|
||||
var rootDirInfo fs.FileInfo
|
||||
if rootDirEntry != nil {
|
||||
rootDirInfo, err = rootDirEntry.Info()
|
||||
}
|
||||
if err == nil && rootDirInfo != nil && filter == "" {
|
||||
// We should not get an error because we created the DirEntry object from the FileInfo object
|
||||
dirList.Files = append(dirList.Files, &sliverpb.FileInfo{
|
||||
Name: ".", // Cannot use the name from the FileInfo / DirEntry because that is the name of the directory
|
||||
@@ -191,6 +197,7 @@ func dirListHandler(data []byte, resp RPCResponse) {
|
||||
if err != nil {
|
||||
// Then this is a bad filter, and it will be a bad filter
|
||||
// on every iteration of the loop, so we might as well break now
|
||||
dirList.Response = &commonpb.Response{Err: err.Error()}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -247,22 +254,26 @@ func getDirList(target string) (string, fs.DirEntry, []fs.DirEntry, error) {
|
||||
// {{end}}
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if rootInfo, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
/*
|
||||
We could place the entry for the directory itself
|
||||
at the beginning of the returned slice of DirEntry
|
||||
objects, but then it is not clear if that is the
|
||||
root directory or a directory / file in the root
|
||||
directory with the same name as the root directory.
|
||||
|
||||
Using WalkDir is not great here because you cannot
|
||||
tell it to not be recursive, so we will be wasting
|
||||
cycles telling it to skip directories and files
|
||||
*/
|
||||
files, err := os.ReadDir(dir)
|
||||
return dir, fs.FileInfoToDirEntry(rootInfo), files, err
|
||||
rootInfo, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return dir, nil, []fs.DirEntry{}, errors.New("directory does not exist")
|
||||
}
|
||||
return dir, nil, []fs.DirEntry{}, err
|
||||
}
|
||||
return dir, nil, []fs.DirEntry{}, errors.New("directory does not exist")
|
||||
/*
|
||||
We could place the entry for the directory itself
|
||||
at the beginning of the returned slice of DirEntry
|
||||
objects, but then it is not clear if that is the
|
||||
root directory or a directory / file in the root
|
||||
directory with the same name as the root directory.
|
||||
|
||||
Using WalkDir is not great here because you cannot
|
||||
tell it to not be recursive, so we will be wasting
|
||||
cycles telling it to skip directories and files
|
||||
*/
|
||||
files, err := os.ReadDir(dir)
|
||||
return dir, fs.FileInfoToDirEntry(rootInfo), files, err
|
||||
}
|
||||
|
||||
func rmHandler(data []byte, resp RPCResponse) {
|
||||
@@ -1146,6 +1157,29 @@ func executeHandler(data []byte, resp RPCResponse) {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(exePath, execReq.Args...)
|
||||
if execReq.EnvInheritance || len(execReq.Env) > 0 {
|
||||
envVars := make(map[string]string)
|
||||
if execReq.EnvInheritance {
|
||||
for _, entry := range os.Environ() {
|
||||
parts := strings.SplitN(entry, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
envVars[parts[0]] = parts[1]
|
||||
} else if len(parts) == 1 {
|
||||
envVars[parts[0]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
maps.Copy(envVars, execReq.Env)
|
||||
keys := make([]string, 0, len(envVars))
|
||||
for key := range envVars {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
cmd.Env = make([]string, 0, len(envVars))
|
||||
for _, key := range keys {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, envVars[key]))
|
||||
}
|
||||
}
|
||||
|
||||
if execReq.Output {
|
||||
stdOutBuff := new(bytes.Buffer)
|
||||
|
||||
@@ -362,9 +362,6 @@ func dnsBeacon(uri *url.URL) *Beacon {
|
||||
return client.WriteEnvelope(envelope)
|
||||
},
|
||||
Close: func() error {
|
||||
if client != nil {
|
||||
return client.CloseSession()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Cleanup: func() error {
|
||||
|
||||
+1027
-1007
File diff suppressed because it is too large.
Load diff
@@ -586,6 +586,9 @@ message WebContent {
|
||||
string ContentType = 4;
|
||||
uint64 Size = 5 [ jstype = JS_STRING ];
|
||||
|
||||
string OriginalFile = 6;
|
||||
string Sha256 = 7;
|
||||
|
||||
bytes Content = 9;
|
||||
}
|
||||
|
||||
@@ -1316,4 +1319,4 @@ message CertificateData {
|
||||
|
||||
message CertificateInfo {
|
||||
repeated CertificateData info = 1;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: commonpb/common.proto
|
||||
|
||||
package commonpb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: dnspb/dns.proto
|
||||
|
||||
package dnspb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v6.33.2
|
||||
// protoc v3.15.8
|
||||
// source: rpcpb/services.proto
|
||||
|
||||
package rpcpb
|
||||
|
||||
+1090
-1063
File diff suppressed because it is too large.
Load diff
@@ -515,6 +515,9 @@ message ExecuteReq {
|
||||
bool Output = 3;
|
||||
string Stdout = 4;
|
||||
string Stderr = 5;
|
||||
bool EnvInheritance = 6;
|
||||
map<string, string> Env = 7;
|
||||
|
||||
uint32 PPid = 10;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
|
||||
+10
-1
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
clientcli "github.com/bishopfox/sliver/client/cli"
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/c2"
|
||||
"github.com/bishopfox/sliver/server/certs"
|
||||
@@ -72,6 +73,8 @@ func initConsoleLogging(appDir string) *os.File {
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().String(clientcli.RCFlagName, "", "path to rc script file")
|
||||
|
||||
// Unpack
|
||||
unpackCmd.Flags().BoolP(forceFlagStr, "f", false, "Force unpack and overwrite")
|
||||
rootCmd.AddCommand(unpackCmd)
|
||||
@@ -147,8 +150,14 @@ var rootCmd = &cobra.Command{
|
||||
if serverConfig.DaemonMode {
|
||||
daemon.Start(daemon.BlankHost, daemon.BlankPort, serverConfig.DaemonConfig.Tailscale)
|
||||
} else {
|
||||
rcScript, err := clientcli.ReadRCScript(cmd)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read rc script: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Args = os.Args[:1] // Hide cli from grumble console
|
||||
console.Start()
|
||||
console.Start(rcScript)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
@@ -47,6 +48,13 @@ var (
|
||||
ErrInvalidDialect = errors.New("invalid SQL Dialect")
|
||||
|
||||
databaseConfigLog = log.NamedLogger("config", "database")
|
||||
|
||||
defaultSQLitePragmas = map[string]string{
|
||||
"journal_mode": "WAL", // reduce writer blocking, better concurrency
|
||||
"busy_timeout": "5000", // wait for locks instead of failing fast (ms)
|
||||
"synchronous": "NORMAL", // faster WAL syncs while retaining durability
|
||||
"temp_store": "MEMORY", // keep temp structures off disk for quicker startup
|
||||
}
|
||||
)
|
||||
|
||||
// GetDatabaseConfigPath - File path to config.json
|
||||
@@ -66,7 +74,8 @@ type DatabaseConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
|
||||
Params map[string]string `json:"params"`
|
||||
Params map[string]string `json:"params"`
|
||||
Pragmas map[string]string `json:"pragmas"`
|
||||
|
||||
MaxIdleConns int `json:"max_idle_conns"`
|
||||
MaxOpenConns int `json:"max_open_conns"`
|
||||
@@ -80,7 +89,7 @@ func (c *DatabaseConfig) DSN() (string, error) {
|
||||
switch c.Dialect {
|
||||
case Sqlite:
|
||||
filePath := filepath.Join(assets.GetRootAppDir(), "sliver.db")
|
||||
params := encodeParams(c.Params)
|
||||
params := encodeSQLiteParams(c.Params, c.Pragmas)
|
||||
return fmt.Sprintf("file:%s?%s", filePath, params), nil
|
||||
case MySQL:
|
||||
user := url.QueryEscape(c.Username)
|
||||
@@ -112,6 +121,44 @@ func encodeParams(rawParams map[string]string) string {
|
||||
return params.Encode()
|
||||
}
|
||||
|
||||
func encodeSQLiteParams(rawParams map[string]string, pragmas map[string]string) string {
|
||||
if rawParams == nil {
|
||||
rawParams = map[string]string{}
|
||||
}
|
||||
params := url.Values{}
|
||||
|
||||
// Preserve any user-provided parameters first.
|
||||
for key, value := range rawParams {
|
||||
params.Add(key, value)
|
||||
}
|
||||
|
||||
// Apply safer defaults only when the user has not set any custom pragma.
|
||||
if _, ok := rawParams["_pragma"]; !ok {
|
||||
selectedPragmas := pragmas
|
||||
if len(selectedPragmas) == 0 {
|
||||
selectedPragmas = defaultSQLitePragmas
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(selectedPragmas))
|
||||
for key := range selectedPragmas {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
value := selectedPragmas[key]
|
||||
params.Add("_pragma", fmt.Sprintf("%s(%s)", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// Encourage shared cache to play nicer with WAL and multiple connections.
|
||||
if _, ok := rawParams["cache"]; !ok {
|
||||
params.Add("cache", "shared")
|
||||
}
|
||||
|
||||
return params.Encode()
|
||||
}
|
||||
|
||||
// Save - Save config file to disk
|
||||
func (c *DatabaseConfig) Save() error {
|
||||
configPath := GetDatabaseConfigPath()
|
||||
@@ -161,6 +208,8 @@ func GetDatabaseConfig() *DatabaseConfig {
|
||||
config.MaxOpenConns = 1
|
||||
}
|
||||
|
||||
ensureSQLiteDefaults(config)
|
||||
|
||||
err := config.Save() // This updates the config with any missing fields
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to save default config %s", err)
|
||||
@@ -171,9 +220,29 @@ func GetDatabaseConfig() *DatabaseConfig {
|
||||
func getDefaultDatabaseConfig() *DatabaseConfig {
|
||||
return &DatabaseConfig{
|
||||
Dialect: Sqlite,
|
||||
Pragmas: defaultSQLitePragmas,
|
||||
MaxIdleConns: 10,
|
||||
MaxOpenConns: 100,
|
||||
|
||||
LogLevel: "warn",
|
||||
}
|
||||
}
|
||||
|
||||
func ensureSQLiteDefaults(c *DatabaseConfig) {
|
||||
if c.Dialect != Sqlite {
|
||||
return
|
||||
}
|
||||
if c.Params == nil {
|
||||
c.Params = map[string]string{}
|
||||
}
|
||||
if _, ok := c.Params["_pragma"]; ok {
|
||||
// User provided explicit pragmas; leave Pragmas untouched to avoid confusion.
|
||||
return
|
||||
}
|
||||
if len(c.Pragmas) == 0 {
|
||||
c.Pragmas = defaultSQLitePragmas
|
||||
}
|
||||
if _, ok := c.Params["cache"]; !ok {
|
||||
c.Params["cache"] = "shared"
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
)
|
||||
|
||||
// Start - Starts the server console
|
||||
func Start() {
|
||||
func Start(rcScript string) {
|
||||
_, ln, _ := transport.LocalListener()
|
||||
ctxDialer := grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return ln.Dial()
|
||||
@@ -57,7 +57,7 @@ func Start() {
|
||||
defer conn.Close()
|
||||
localRPC := rpcpb.NewSliverRPCClient(conn)
|
||||
con := console.NewConsole(false)
|
||||
console.StartClient(con, localRPC, command.ServerCommands(con, serverOnlyCmds), command.SliverCommands(con), true)
|
||||
console.StartClient(con, localRPC, command.ServerCommands(con, serverOnlyCmds), command.SliverCommands(con), true, rcScript)
|
||||
|
||||
con.App.Start()
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
Rtunnels map[uint64]*RTunnel = make(map[uint64]*RTunnel)
|
||||
mutex sync.RWMutex
|
||||
pending sync.Map
|
||||
Rtunnels map[uint64]*RTunnel = make(map[uint64]*RTunnel)
|
||||
mutex sync.RWMutex
|
||||
pending map[string]map[string]int
|
||||
listeners map[string]map[uint32]string
|
||||
pendingMu sync.Mutex
|
||||
)
|
||||
|
||||
// RTunnel - Duplex byte read/write
|
||||
@@ -97,20 +99,107 @@ func RemoveRTunnel(ID uint64) {
|
||||
}
|
||||
|
||||
func AddPending(sessionID string, connStr string) {
|
||||
pending.Store(sessionID, connStr)
|
||||
pendingMu.Lock()
|
||||
defer pendingMu.Unlock()
|
||||
if pending == nil {
|
||||
pending = make(map[string]map[string]int)
|
||||
}
|
||||
addrMap := pending[sessionID]
|
||||
if addrMap == nil {
|
||||
addrMap = make(map[string]int)
|
||||
pending[sessionID] = addrMap
|
||||
}
|
||||
addrMap[connStr]++
|
||||
}
|
||||
|
||||
func DeletePending(sessionID string) {
|
||||
pending.Delete(sessionID)
|
||||
func DeletePending(sessionID string, connStr string) {
|
||||
pendingMu.Lock()
|
||||
defer pendingMu.Unlock()
|
||||
if pending == nil {
|
||||
return
|
||||
}
|
||||
if addrMap, ok := pending[sessionID]; ok {
|
||||
if count, ok := addrMap[connStr]; ok {
|
||||
if count <= 1 {
|
||||
delete(addrMap, connStr)
|
||||
} else {
|
||||
addrMap[connStr] = count - 1
|
||||
}
|
||||
}
|
||||
if len(addrMap) == 0 {
|
||||
delete(pending, sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Check(sessionID string, connStr string) bool {
|
||||
if val, ok := pending.Load(sessionID); ok {
|
||||
return val == connStr
|
||||
pendingMu.Lock()
|
||||
defer pendingMu.Unlock()
|
||||
if pending == nil {
|
||||
return false
|
||||
}
|
||||
if addrMap, ok := pending[sessionID]; ok {
|
||||
return addrMap[connStr] > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TrackListener(sessionID string, listenerID uint32, connStr string) {
|
||||
pendingMu.Lock()
|
||||
defer pendingMu.Unlock()
|
||||
if listeners == nil {
|
||||
listeners = make(map[string]map[uint32]string)
|
||||
}
|
||||
if pending == nil {
|
||||
pending = make(map[string]map[string]int)
|
||||
}
|
||||
lm := listeners[sessionID]
|
||||
if lm == nil {
|
||||
lm = make(map[uint32]string)
|
||||
listeners[sessionID] = lm
|
||||
}
|
||||
lm[listenerID] = connStr
|
||||
addrMap := pending[sessionID]
|
||||
if addrMap == nil {
|
||||
addrMap = make(map[string]int)
|
||||
pending[sessionID] = addrMap
|
||||
}
|
||||
addrMap[connStr]++
|
||||
}
|
||||
|
||||
func UntrackListener(sessionID string, listenerID uint32) bool {
|
||||
pendingMu.Lock()
|
||||
defer pendingMu.Unlock()
|
||||
if listeners == nil || pending == nil {
|
||||
return false
|
||||
}
|
||||
lm, ok := listeners[sessionID]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
connStr, ok := lm[listenerID]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
delete(lm, listenerID)
|
||||
if len(lm) == 0 {
|
||||
delete(listeners, sessionID)
|
||||
}
|
||||
if addrMap, ok := pending[sessionID]; ok {
|
||||
if count, ok := addrMap[connStr]; ok {
|
||||
if count <= 1 {
|
||||
delete(addrMap, connStr)
|
||||
} else {
|
||||
addrMap[connStr] = count - 1
|
||||
}
|
||||
}
|
||||
if len(addrMap) == 0 {
|
||||
delete(pending, sessionID)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// func removeAndCloseAllRTunnels() {
|
||||
// mutex.Lock()
|
||||
// defer mutex.Unlock()
|
||||
|
||||
+12
-2
@@ -789,8 +789,18 @@ func AddContent(pbWebContent *clientpb.WebContent, webContentDir string) (*clien
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dbWebContent.ContentType = pbWebContent.ContentType
|
||||
dbWebContent.Size = pbWebContent.Size
|
||||
if pbWebContent.ContentType != "" {
|
||||
dbWebContent.ContentType = pbWebContent.ContentType
|
||||
}
|
||||
if pbWebContent.Size != 0 {
|
||||
dbWebContent.Size = pbWebContent.Size
|
||||
}
|
||||
if pbWebContent.OriginalFile != "" {
|
||||
dbWebContent.OriginalFile = pbWebContent.OriginalFile
|
||||
}
|
||||
if pbWebContent.Sha256 != "" {
|
||||
dbWebContent.Sha256 = pbWebContent.Sha256
|
||||
}
|
||||
|
||||
dbModelWebContent := models.WebContentFromProtobuf(dbWebContent)
|
||||
err = Session().Save(&dbModelWebContent).Error
|
||||
|
||||
+20
-14
@@ -67,9 +67,11 @@ type WebContent struct {
|
||||
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
|
||||
WebsiteID uuid.UUID `gorm:"type:uuid;"`
|
||||
|
||||
Path string `gorm:"primaryKey"`
|
||||
Size uint64
|
||||
ContentType string
|
||||
Path string `gorm:"primaryKey"`
|
||||
Size uint64
|
||||
ContentType string
|
||||
OriginalFile string
|
||||
Sha256 string
|
||||
}
|
||||
|
||||
// BeforeCreate - GORM hook to automatically set values
|
||||
@@ -81,12 +83,14 @@ func (wc *WebContent) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
// ToProtobuf - Converts to protobuf object
|
||||
func (wc *WebContent) ToProtobuf(content *[]byte) *clientpb.WebContent {
|
||||
return &clientpb.WebContent{
|
||||
ID: wc.ID.String(),
|
||||
WebsiteID: wc.WebsiteID.String(),
|
||||
Path: wc.Path,
|
||||
Size: uint64(wc.Size),
|
||||
ContentType: wc.ContentType,
|
||||
Content: *content,
|
||||
ID: wc.ID.String(),
|
||||
WebsiteID: wc.WebsiteID.String(),
|
||||
Path: wc.Path,
|
||||
Size: uint64(wc.Size),
|
||||
ContentType: wc.ContentType,
|
||||
OriginalFile: wc.OriginalFile,
|
||||
Sha256: wc.Sha256,
|
||||
Content: *content,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +99,12 @@ func WebContentFromProtobuf(pbWebContent *clientpb.WebContent) WebContent {
|
||||
websiteUUID, _ := uuid.FromString(pbWebContent.WebsiteID)
|
||||
|
||||
return WebContent{
|
||||
ID: siteUUID,
|
||||
WebsiteID: websiteUUID,
|
||||
Path: pbWebContent.Path,
|
||||
Size: pbWebContent.Size,
|
||||
ContentType: pbWebContent.ContentType,
|
||||
ID: siteUUID,
|
||||
WebsiteID: websiteUUID,
|
||||
Path: pbWebContent.Path,
|
||||
Size: pbWebContent.Size,
|
||||
ContentType: pbWebContent.ContentType,
|
||||
OriginalFile: pbWebContent.OriginalFile,
|
||||
Sha256: pbWebContent.Sha256,
|
||||
}
|
||||
}
|
||||
@@ -533,12 +533,23 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
goPackage = config.GoPackage
|
||||
}
|
||||
}
|
||||
result, err := goname.RenameModule(sliverPkgDir, goPackage)
|
||||
modResult, err := goname.RenameModule(sliverPkgDir, goPackage)
|
||||
if err != nil {
|
||||
buildLog.Errorf("Failed to rename module: %s", err)
|
||||
return "", err
|
||||
}
|
||||
buildLog.Infof("Renamed module: %s -> %s (go.mod updated: %v)", result.OldModule, result.NewModule, result.GoModUpdated)
|
||||
buildLog.Infof("Renamed module: %s -> %s (go.mod updated: %v)", modResult.OldModule, modResult.NewModule, modResult.GoModUpdated)
|
||||
|
||||
// Rename Import Paths
|
||||
buildLog.Infof("Renaming import paths ...")
|
||||
oldImportPrefix := path.Join(modResult.NewModule, "implant", "sliver")
|
||||
newImportPrefix := path.Join(modResult.NewModule, "runc", "cgroup")
|
||||
importResult, err := goname.RenameImport(sliverPkgDir, oldImportPrefix, newImportPrefix)
|
||||
if err != nil {
|
||||
buildLog.Errorf("Failed to rename imports: %s", err)
|
||||
return "", err
|
||||
}
|
||||
buildLog.Infof("Renamed imports (%d files updated)", importResult.FilesUpdated)
|
||||
}
|
||||
|
||||
return sliverPkgDir, nil
|
||||
|
||||
@@ -2,7 +2,10 @@ package goname
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
@@ -37,6 +40,9 @@ func RenameImport(dir, oldImportPrefix, newImportPrefix string) (*ImportResult,
|
||||
if result.ImportsUpdated == 0 {
|
||||
return result, errors.New("no imports updated (check old/new import prefixes)")
|
||||
}
|
||||
if err := renameImportDir(dir, oldImportPrefix, newImportPrefix); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -68,3 +74,61 @@ func rewriteImportPrefixes(dir, oldImportPrefix, newImportPrefix string, result
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func renameImportDir(dir, oldImportPrefix, newImportPrefix string) error {
|
||||
oldParts := splitImportPrefix(oldImportPrefix)
|
||||
newParts := splitImportPrefix(newImportPrefix)
|
||||
if len(oldParts) == 0 || len(newParts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < len(oldParts); i++ {
|
||||
suffixParts := oldParts[i:]
|
||||
oldDir := filepath.Join(append([]string{dir}, suffixParts...)...)
|
||||
info, err := os.Stat(oldDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(newParts) < len(suffixParts) {
|
||||
return fmt.Errorf("new import prefix has fewer path segments than old import prefix")
|
||||
}
|
||||
newSuffixParts := newParts[len(newParts)-len(suffixParts):]
|
||||
newDir := filepath.Join(append([]string{dir}, newSuffixParts...)...)
|
||||
|
||||
if oldDir == newDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(newDir); err == nil {
|
||||
return fmt.Errorf("rename import dir: target exists: %s", newDir)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(newDir), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(oldDir, newDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitImportPrefix(prefix string) []string {
|
||||
cleaned := path.Clean(strings.TrimSpace(prefix))
|
||||
if cleaned == "." || cleaned == "/" {
|
||||
return nil
|
||||
}
|
||||
cleaned = strings.TrimPrefix(cleaned, "/")
|
||||
if cleaned == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(cleaned, "/")
|
||||
}
|
||||
@@ -42,11 +42,15 @@ func TestRenameImportBasic(t *testing.T) {
|
||||
t.Fatalf("main.go still references old prefix")
|
||||
}
|
||||
|
||||
barImports := importPaths(t, filepath.Join(tmp, "foo", "bar", "bar.go"))
|
||||
barImports := importPaths(t, filepath.Join(tmp, "acme", "corp", "bar.go"))
|
||||
if !barImports["acme/corp/baz"] {
|
||||
t.Fatalf("bar.go import not updated")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(tmp, "foo", "bar")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected foo/bar to be renamed")
|
||||
}
|
||||
|
||||
vendorImports := importPaths(t, filepath.Join(tmp, "vendor", "foo", "bar", "vendor.go"))
|
||||
if !vendorImports["foo/bar/baz"] {
|
||||
t.Fatalf("vendor import should remain unchanged")
|
||||
|
||||
@@ -231,7 +231,6 @@ func createReverseTunnelHandler(implantConn *core.ImplantConnection, data []byte
|
||||
sessionHandlerLog.Errorf("Session %s attempted to create reverse tunnel to %s without being initiated by a client", session.ID, remoteAddress)
|
||||
return nil
|
||||
}
|
||||
defer rtunnels.DeletePending(session.ID)
|
||||
|
||||
var defaultDialer = new(net.Dialer)
|
||||
|
||||
|
||||
@@ -109,6 +109,12 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
|
||||
if err != nil {
|
||||
req.Config.Format = clientpb.OutputFormat_SHELLCODE
|
||||
req.Config.ObfuscateSymbols = false
|
||||
req.Config.IsShellcode = true
|
||||
req.Config.IsSharedLib = false
|
||||
req.Config.TemplateName = "sliver"
|
||||
if len(req.Config.Exports) == 0 {
|
||||
req.Config.Exports = []string{"StartW"}
|
||||
}
|
||||
build, err := generate.GenerateConfig(name, req.Config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
|
||||
@@ -39,11 +39,17 @@ func (rpc *Server) GetRportFwdListeners(ctx context.Context, req *sliverpb.Rport
|
||||
// StartRportfwdListener - Instruct the implant to start a reverse port forward
|
||||
func (rpc *Server) StartRportFwdListener(ctx context.Context, req *sliverpb.RportFwdStartListenerReq) (*sliverpb.RportFwdListener, error) {
|
||||
resp := &sliverpb.RportFwdListener{Response: &commonpb.Response{}}
|
||||
rtunnels.AddPending(req.Request.SessionID, req.ForwardAddress)
|
||||
err := rpc.GenericHandler(req, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Response.GetErr() == "" {
|
||||
addr := req.ForwardAddress
|
||||
if resp.ForwardAddress != "" {
|
||||
addr = resp.ForwardAddress
|
||||
}
|
||||
rtunnels.TrackListener(req.Request.SessionID, resp.ID, addr)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -54,5 +60,8 @@ func (rpc *Server) StopRportFwdListener(ctx context.Context, req *sliverpb.Rport
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Response.GetErr() == "" {
|
||||
rtunnels.UntrackListener(req.Request.SessionID, req.ID)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package website
|
||||
*/
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -87,14 +89,30 @@ func AddContent(name string, pbWebContent *clientpb.WebContent) error {
|
||||
pbWebContent.WebsiteID = website.ID
|
||||
}
|
||||
|
||||
if pbWebContent.Size == 0 && len(pbWebContent.Content) > 0 {
|
||||
pbWebContent.Size = uint64(len(pbWebContent.Content))
|
||||
}
|
||||
if pbWebContent.OriginalFile == "" {
|
||||
pbWebContent.OriginalFile = filepath.Base(pbWebContent.Path)
|
||||
}
|
||||
|
||||
if len(pbWebContent.Content) > 0 {
|
||||
sha := sha256.Sum256(pbWebContent.Content)
|
||||
pbWebContent.Sha256 = hex.EncodeToString(sha[:])
|
||||
}
|
||||
|
||||
webContent, err := db.AddContent(pbWebContent, webContentDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write content to disk
|
||||
webContentPath := filepath.Join(webContentDir, webContent.ID)
|
||||
return os.WriteFile(webContentPath, pbWebContent.Content, 0600)
|
||||
// Write content to disk when provided (metadata-only updates skip disk writes)
|
||||
if len(pbWebContent.Content) > 0 {
|
||||
webContentPath := filepath.Join(webContentDir, webContent.ID)
|
||||
return os.WriteFile(webContentPath, pbWebContent.Content, 0600)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveContent - Remove website content for a path
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# This is the official list of age authors for copyright purposes.
|
||||
# To be included, send a change adding the individual or company
|
||||
# who owns a contribution's copyright.
|
||||
|
||||
Google LLC
|
||||
Filippo Valsorda
|
||||
+2
@@ -1,4 +1,6 @@
|
||||
Copyright 2019 The age Authors
|
||||
Copyright 2019 Google LLC
|
||||
Copyright 2022 Filippo Valsorda
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
||||
+67
-45
@@ -12,7 +12,7 @@
|
||||
|
||||
age is a simple, modern and secure file encryption tool, format, and Go library.
|
||||
|
||||
It features small explicit keys, no config options, and UNIX-style composability.
|
||||
It features small explicit keys, post-quantum support, no config options, and UNIX-style composability.
|
||||
|
||||
```
|
||||
$ age-keygen -o key.txt
|
||||
@@ -21,17 +21,17 @@ $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9
|
||||
$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
```
|
||||
|
||||
📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
|
||||
|
||||
📬 Follow the maintenance of this project by subscribing to [Maintainer Dispatches](https://filippo.io/newsletter)!
|
||||
📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@benjojo](https://github.com/benjojo) and [@FiloSottile](https://github.com/FiloSottile).
|
||||
|
||||
🦀 An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
|
||||
|
||||
🌍 [Typage](https://github.com/FiloSottile/typage) is a TypeScript implementation. It works in the browser, Node.js, Deno, and Bun.
|
||||
|
||||
🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin.
|
||||
|
||||
✨ For more plugins, implementations, tools, and integrations, check out the [awesome age](https://github.com/FiloSottile/awesome-age) list.
|
||||
|
||||
💬 The author pronounces it `[aɡe̞]` [with a hard *g*](https://translate.google.com/?sl=it&text=aghe), like GIF, and is always spelled lowercase.
|
||||
💬 The author pronounces it `[aɡe̞]` [with a hard *g*](https://translate.google.com/?sl=it&text=aghe), like GIF, and it's always spelled lowercase.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -48,6 +48,12 @@ $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
<code>port install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td>
|
||||
<code>winget install --id FiloSottile.age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alpine Linux v3.15+</td>
|
||||
<td>
|
||||
@@ -85,6 +91,12 @@ $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
<code>emerge app-crypt/age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Guix System</td>
|
||||
<td>
|
||||
<code>guix package -i age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NixOS / Nix</td>
|
||||
<td>
|
||||
@@ -133,22 +145,18 @@ $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz
|
||||
<code>scoop bucket add extras && scoop install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>pkgx</td>
|
||||
<td>
|
||||
<code>pkgx install age</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
On Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries.
|
||||
|
||||
```
|
||||
https://dl.filippo.io/age/latest?for=linux/amd64
|
||||
https://dl.filippo.io/age/v1.1.1?for=darwin/arm64
|
||||
https://dl.filippo.io/age/v1.3.0?for=darwin/arm64
|
||||
...
|
||||
```
|
||||
|
||||
If you download the pre-built binaries, you can check their [Sigsum proofs](./SIGSUM.md).
|
||||
|
||||
If your system has [a supported version of Go](https://go.dev/dl/), you can build from source.
|
||||
|
||||
```
|
||||
@@ -157,39 +165,6 @@ go install filippo.io/age/cmd/...@latest
|
||||
|
||||
Help from new packagers is very welcome.
|
||||
|
||||
### Verifying the release signatures
|
||||
|
||||
If you download the pre-built binaries, you can check their
|
||||
[Sigsum](https://www.sigsum.org) proofs, which are like signatures with extra
|
||||
transparency: you can cryptographically verify that every proof is logged in a
|
||||
public append-only log, so you can hold the age project accountable for every
|
||||
binary release we ever produced. This is similar to what the [Go Checksum
|
||||
Database](https://go.dev/blog/module-mirror-launch) provides.
|
||||
|
||||
```
|
||||
cat << EOF > age-sigsum-key.pub
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1WpnEswJLPzvXJDiswowy48U+G+G1kmgwUE2eaRHZG
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAz2WM5CyPLqiNjk7CLl4roDXwKhQ0QExXLebukZEZFS
|
||||
EOF
|
||||
cat << EOF > sigsum-trust-policy.txt
|
||||
log 154f49976b59ff09a123675f58cb3e346e0455753c3c3b15d465dcb4f6512b0b https://poc.sigsum.org/jellyfish
|
||||
witness poc.sigsum.org/nisse 1c25f8a44c635457e2e391d1efbca7d4c2951a0aef06225a881e46b98962ac6c
|
||||
witness rgdd.se/poc-witness 28c92a5a3a054d317c86fc2eeb6a7ab2054d6217100d0be67ded5b74323c5806
|
||||
group demo-quorum-rule all poc.sigsum.org/nisse rgdd.se/poc-witness
|
||||
quorum demo-quorum-rule
|
||||
EOF
|
||||
|
||||
curl -JLO "https://dl.filippo.io/age/v1.2.0?for=darwin/arm64"
|
||||
curl -JLO "https://dl.filippo.io/age/v1.2.0?for=darwin/arm64&proof"
|
||||
|
||||
go install sigsum.org/sigsum-go/cmd/sigsum-verify@v0.8.0
|
||||
sigsum-verify -k age-sigsum-key.pub -p sigsum-trust-policy.txt \
|
||||
age-v1.2.0-darwin-arm64.tar.gz.proof < age-v1.2.0-darwin-arm64.tar.gz
|
||||
```
|
||||
|
||||
You can learn more about what's happening above in the [Sigsum
|
||||
docs](https://www.sigsum.org/getting-started/).
|
||||
|
||||
## Usage
|
||||
|
||||
For the full documentation, read [the age(1) man page](https://filippo.io/age/age.1).
|
||||
@@ -254,6 +229,28 @@ $ age -R recipients.txt example.jpg > example.jpg.age
|
||||
|
||||
If the argument to `-R` (or `-i`) is `-`, the file is read from standard input.
|
||||
|
||||
### Post-quantum keys
|
||||
|
||||
To generate hybrid post-quantum keys, which are secure against future quantum
|
||||
computer attacks, use the `-pq` flag with `age-keygen`. This may become the
|
||||
default in the future.
|
||||
|
||||
Post-quantum identities start with `AGE-SECRET-KEY-PQ-1...` and recipients with
|
||||
`age1pq1...`. The recipients are unfortunately ~2000 characters long.
|
||||
|
||||
```
|
||||
$ age-keygen -pq -o key.txt
|
||||
$ age-keygen -y key.txt > recipient.txt
|
||||
$ age -R recipient.txt example.jpg > example.jpg.age
|
||||
$ age -d -i key.txt example.jpg.age > example.jpg
|
||||
```
|
||||
|
||||
Support for post-quantum keys is built into age v1.3.0 and later. Alternatively,
|
||||
the `age-plugin-pq` binary can be installed and placed in `$PATH` to add support
|
||||
to any version and implementation of age that supports plugins. Recipients will
|
||||
work out of the box, while identities will have to be converted to plugin
|
||||
identities with `age-plugin-pq -identity`.
|
||||
|
||||
### Passphrases
|
||||
|
||||
Files can be encrypted with a passphrase by using `-p/--passphrase`. By default age will automatically generate a secure passphrase. Passphrase protected files are automatically detected at decrypt time.
|
||||
@@ -302,3 +299,28 @@ $ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
|
||||
```
|
||||
|
||||
Keep in mind that people might not protect SSH keys long-term, since they are revokable when used only for authentication, and that SSH keys held on YubiKeys can't be used to decrypt files.
|
||||
|
||||
### Inspecting encrypted files
|
||||
|
||||
The `age-inspect` command can display metadata about an encrypted file without decrypting it, including the recipient types, whether it uses post-quantum encryption, and the payload size.
|
||||
|
||||
```
|
||||
$ age-inspect secrets.age
|
||||
secrets.age is an age file, version "age-encryption.org/v1".
|
||||
|
||||
This file is encrypted to the following recipient types:
|
||||
- "mlkem768x25519"
|
||||
|
||||
This file uses post-quantum encryption.
|
||||
|
||||
Size breakdown (assuming it decrypts successfully):
|
||||
|
||||
Header 1627 bytes
|
||||
Encryption overhead 32 bytes
|
||||
Payload 42 bytes
|
||||
-------------------
|
||||
Total 1701 bytes
|
||||
|
||||
```
|
||||
|
||||
For scripting, use `--json` to get machine-readable output.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
If you download the pre-built binaries of version v1.2.0+, you can check their
|
||||
[Sigsum](https://www.sigsum.org) proofs, which are like signatures with extra
|
||||
transparency: you can cryptographically verify that every proof is logged in a
|
||||
public append-only log, so the age project can be held accountable for every
|
||||
binary release we ever produced. This is similar to what the [Go Checksum
|
||||
Database](https://go.dev/blog/module-mirror-launch) provides.
|
||||
|
||||
```
|
||||
cat << EOF > age-sigsum-key.pub
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1WpnEswJLPzvXJDiswowy48U+G+G1kmgwUE2eaRHZG
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAz2WM5CyPLqiNjk7CLl4roDXwKhQ0QExXLebukZEZFS
|
||||
EOF
|
||||
|
||||
curl -JLO "https://dl.filippo.io/age/v1.3.0?for=darwin/arm64"
|
||||
curl -JLO "https://dl.filippo.io/age/v1.3.0?for=darwin/arm64&proof"
|
||||
|
||||
go install sigsum.org/sigsum-go/cmd/sigsum-verify@v0.13.1
|
||||
sigsum-verify -k age-sigsum-key.pub -P sigsum-generic-2025-1 \
|
||||
age-v1.3.0-darwin-arm64.tar.gz.proof < age-v1.3.0-darwin-arm64.tar.gz
|
||||
```
|
||||
|
||||
You can learn more about what's happening above in the [Sigsum
|
||||
docs](https://www.sigsum.org/getting-started/).
|
||||
+256
-69
@@ -5,10 +5,10 @@
|
||||
// Package age implements file encryption according to the age-encryption.org/v1
|
||||
// specification.
|
||||
//
|
||||
// For most use cases, use the Encrypt and Decrypt functions with
|
||||
// X25519Recipient and X25519Identity. If passphrase encryption is required, use
|
||||
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
|
||||
// use the filippo.io/age/agessh package.
|
||||
// For most use cases, use the [Encrypt] and [Decrypt] functions with
|
||||
// [HybridRecipient] and [HybridIdentity]. If passphrase encryption is
|
||||
// required, use [ScryptRecipient] and [ScryptIdentity]. For compatibility with
|
||||
// existing SSH keys use the filippo.io/age/agessh package.
|
||||
//
|
||||
// age encrypted files are binary and not malleable. For encoding them as text,
|
||||
// use the filippo.io/age/armor package.
|
||||
@@ -26,13 +26,13 @@
|
||||
// There is no default path for age keys. Instead, they should be stored at
|
||||
// application-specific paths. The CLI supports files where private keys are
|
||||
// listed one per line, ignoring empty lines and lines starting with "#". These
|
||||
// files can be parsed with ParseIdentities.
|
||||
// files can be parsed with [ParseIdentities].
|
||||
//
|
||||
// When integrating age into a new system, it's recommended that you only
|
||||
// support X25519 keys, and not SSH keys. The latter are supported for manual
|
||||
// encryption operations. If you need to tie into existing key management
|
||||
// infrastructure, you might want to consider implementing your own Recipient
|
||||
// and Identity.
|
||||
// support native (X25519 and hybrid) keys, and not SSH keys. The latter are
|
||||
// supported for manual encryption operations. If you need to tie into existing
|
||||
// key management infrastructure, you might want to consider implementing your
|
||||
// own [Recipient] and [Identity].
|
||||
//
|
||||
// # Backwards compatibility
|
||||
//
|
||||
@@ -46,47 +46,47 @@
|
||||
package age
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
"filippo.io/age/internal/stream"
|
||||
)
|
||||
|
||||
// An Identity is passed to Decrypt to unwrap an opaque file key from a
|
||||
// recipient stanza. It can be for example a secret key like X25519Identity, a
|
||||
// An Identity is passed to [Decrypt] to unwrap an opaque file key from a
|
||||
// recipient stanza. It can be for example a secret key like [HybridIdentity], a
|
||||
// plugin, or a custom implementation.
|
||||
//
|
||||
// Unwrap must return an error wrapping ErrIncorrectIdentity if none of the
|
||||
// recipient stanzas match the identity, any other error will be considered
|
||||
// fatal.
|
||||
//
|
||||
// Most age API users won't need to interact with this directly, and should
|
||||
// instead pass Recipient implementations to Encrypt and Identity
|
||||
// implementations to Decrypt.
|
||||
type Identity interface {
|
||||
// Unwrap must return an error wrapping [ErrIncorrectIdentity] if none of
|
||||
// the recipient stanzas match the identity, any other error will be
|
||||
// considered fatal.
|
||||
//
|
||||
// Most age API users won't need to interact with this method directly, and
|
||||
// should instead pass [Identity] implementations to [Decrypt].
|
||||
Unwrap(stanzas []*Stanza) (fileKey []byte, err error)
|
||||
}
|
||||
|
||||
// ErrIncorrectIdentity is returned by [Identity.Unwrap] if none of the
|
||||
// recipient stanzas match the identity.
|
||||
var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block")
|
||||
|
||||
// A Recipient is passed to Encrypt to wrap an opaque file key to one or more
|
||||
// recipient stanza(s). It can be for example a public key like X25519Recipient,
|
||||
// A Recipient is passed to [Encrypt] to wrap an opaque file key to one or more
|
||||
// recipient stanza(s). It can be for example a public key like [HybridRecipient],
|
||||
// a plugin, or a custom implementation.
|
||||
//
|
||||
// Most age API users won't need to interact with this directly, and should
|
||||
// instead pass Recipient implementations to Encrypt and Identity
|
||||
// implementations to Decrypt.
|
||||
type Recipient interface {
|
||||
// Most age API users won't need to interact with this method directly, and
|
||||
// should instead pass [Recipient] implementations to [Encrypt].
|
||||
Wrap(fileKey []byte) ([]*Stanza, error)
|
||||
}
|
||||
|
||||
// RecipientWithLabels can be optionally implemented by a Recipient, in which
|
||||
// case Encrypt will use WrapWithLabels instead of Wrap.
|
||||
// RecipientWithLabels can be optionally implemented by a [Recipient], in which
|
||||
// case [Encrypt] will use WrapWithLabels instead of [Recipient.Wrap].
|
||||
//
|
||||
// Encrypt will succeed only if the labels returned by all the recipients
|
||||
// (assuming the empty set for those that don't implement RecipientWithLabels)
|
||||
@@ -103,9 +103,9 @@ type RecipientWithLabels interface {
|
||||
// A Stanza is a section of the age header that encapsulates the file key as
|
||||
// encrypted to a specific recipient.
|
||||
//
|
||||
// Most age API users won't need to interact with this directly, and should
|
||||
// instead pass Recipient implementations to Encrypt and Identity
|
||||
// implementations to Decrypt.
|
||||
// Most age API users won't need to interact with this type directly, and should
|
||||
// instead pass [Recipient] implementations to [Encrypt] and [Identity]
|
||||
// implementations to [Decrypt].
|
||||
type Stanza struct {
|
||||
Type string
|
||||
Args []string
|
||||
@@ -115,35 +115,23 @@ type Stanza struct {
|
||||
const fileKeySize = 16
|
||||
const streamNonceSize = 16
|
||||
|
||||
// Encrypt encrypts a file to one or more recipients.
|
||||
//
|
||||
// Writes to the returned WriteCloser are encrypted and written to dst as an age
|
||||
// file. Every recipient will be able to decrypt the file.
|
||||
//
|
||||
// The caller must call Close on the WriteCloser when done for the last chunk to
|
||||
// be encrypted and flushed to dst.
|
||||
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
func encryptHdr(fileKey []byte, recipients ...Recipient) (*format.Header, error) {
|
||||
if len(recipients) == 0 {
|
||||
return nil, errors.New("no recipients specified")
|
||||
}
|
||||
|
||||
fileKey := make([]byte, fileKeySize)
|
||||
if _, err := rand.Read(fileKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hdr := &format.Header{}
|
||||
var labels []string
|
||||
for i, r := range recipients {
|
||||
stanzas, l, err := wrapWithLabels(r, fileKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
|
||||
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %w", i, err)
|
||||
}
|
||||
sort.Strings(l)
|
||||
if i == 0 {
|
||||
labels = l
|
||||
} else if !slicesEqual(labels, l) {
|
||||
return nil, fmt.Errorf("incompatible recipients")
|
||||
return nil, incompatibleLabelsError(labels, l)
|
||||
}
|
||||
for _, s := range stanzas {
|
||||
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
|
||||
@@ -154,19 +142,62 @@ func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
} else {
|
||||
hdr.MAC = mac
|
||||
}
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts a file to one or more recipients. Every recipient will be
|
||||
// able to decrypt the file.
|
||||
//
|
||||
// Writes to the returned WriteCloser are encrypted and written to dst as an age
|
||||
// file. The caller must call Close on the WriteCloser when done for the last
|
||||
// chunk to be encrypted and flushed to dst.
|
||||
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
|
||||
fileKey := make([]byte, fileKeySize)
|
||||
rand.Read(fileKey)
|
||||
|
||||
hdr, err := encryptHdr(fileKey, recipients...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := hdr.Marshal(dst); err != nil {
|
||||
return nil, fmt.Errorf("failed to write header: %v", err)
|
||||
return nil, fmt.Errorf("failed to write header: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rand.Read(nonce)
|
||||
if _, err := dst.Write(nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to write nonce: %v", err)
|
||||
return nil, fmt.Errorf("failed to write nonce: %w", err)
|
||||
}
|
||||
|
||||
return stream.NewWriter(streamKey(fileKey, nonce), dst)
|
||||
return stream.NewEncryptWriter(streamKey(fileKey, nonce), dst)
|
||||
}
|
||||
|
||||
// EncryptReader encrypts a file to one or more recipients. Every recipient will be
|
||||
// able to decrypt the file.
|
||||
//
|
||||
// Reads from the returned Reader produce the encrypted file, where the plaintext
|
||||
// is read from src.
|
||||
func EncryptReader(src io.Reader, recipients ...Recipient) (io.Reader, error) {
|
||||
fileKey := make([]byte, fileKeySize)
|
||||
rand.Read(fileKey)
|
||||
|
||||
hdr, err := encryptHdr(fileKey, recipients...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := hdr.Marshal(buf); err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare header: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
rand.Read(nonce)
|
||||
|
||||
r, err := stream.NewEncryptReader(streamKey(fileKey, nonce), src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.MultiReader(buf, bytes.NewReader(nonce), r), nil
|
||||
}
|
||||
|
||||
func wrapWithLabels(r Recipient, fileKey []byte) (s []*Stanza, labels []string, err error) {
|
||||
@@ -189,39 +220,147 @@ func slicesEqual(s1, s2 []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NoIdentityMatchError is returned by Decrypt when none of the supplied
|
||||
func incompatibleLabelsError(l1, l2 []string) error {
|
||||
hasPQ1 := slices.Contains(l1, "postquantum")
|
||||
hasPQ2 := slices.Contains(l2, "postquantum")
|
||||
if hasPQ1 != hasPQ2 {
|
||||
return fmt.Errorf("incompatible recipients: can't mix post-quantum and classic recipients, or the file would be vulnerable to quantum computers")
|
||||
}
|
||||
return fmt.Errorf("incompatible recipients: %q and %q can't be mixed", l1, l2)
|
||||
}
|
||||
|
||||
// NoIdentityMatchError is returned by [Decrypt] when none of the supplied
|
||||
// identities match the encrypted file.
|
||||
type NoIdentityMatchError struct {
|
||||
// Errors is a slice of all the errors returned to Decrypt by the Unwrap
|
||||
// calls it made. They all wrap ErrIncorrectIdentity.
|
||||
// calls it made. They all wrap [ErrIncorrectIdentity].
|
||||
Errors []error
|
||||
// StanzaTypes are the first argument of each recipient stanza in the
|
||||
// encrypted file's header.
|
||||
StanzaTypes []string
|
||||
}
|
||||
|
||||
func (*NoIdentityMatchError) Error() string {
|
||||
func (e *NoIdentityMatchError) Error() string {
|
||||
if len(e.Errors) == 1 {
|
||||
return "identity did not match any of the recipients: " + e.Errors[0].Error()
|
||||
}
|
||||
return "no identity matched any of the recipients"
|
||||
}
|
||||
|
||||
// Decrypt decrypts a file encrypted to one or more identities.
|
||||
//
|
||||
// It returns a Reader reading the decrypted plaintext of the age file read
|
||||
// from src. All identities will be tried until one successfully decrypts the file.
|
||||
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
if len(identities) == 0 {
|
||||
return nil, errors.New("no identities specified")
|
||||
}
|
||||
func (e *NoIdentityMatchError) Unwrap() []error {
|
||||
return e.Errors
|
||||
}
|
||||
|
||||
// Decrypt decrypts a file encrypted to one or more identities.
|
||||
// All identities will be tried until one successfully decrypts the file.
|
||||
// Native, non-interactive identities are tried before any other identities.
|
||||
//
|
||||
// Decrypt returns a Reader reading the decrypted plaintext of the age file read
|
||||
// from src. If no identity matches the encrypted file, the returned error will
|
||||
// be of type [NoIdentityMatchError].
|
||||
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
hdr, payload, err := format.Parse(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
|
||||
fileKey, err := decryptHdr(hdr, identities...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := io.ReadFull(payload, nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to read nonce: %w", err)
|
||||
}
|
||||
|
||||
return stream.NewDecryptReader(streamKey(fileKey, nonce), payload)
|
||||
}
|
||||
|
||||
// DecryptReaderAt decrypts a file encrypted to one or more identities.
|
||||
// All identities will be tried until one successfully decrypts the file.
|
||||
// Native, non-interactive identities are tried before any other identities.
|
||||
//
|
||||
// DecryptReaderAt takes an underlying [io.ReaderAt] and its total encrypted
|
||||
// size, and returns a ReaderAt of the decrypted plaintext and the plaintext
|
||||
// size. These can be used for example to instantiate an [io.SectionReader],
|
||||
// which implements [io.Reader] and [io.Seeker], or for [zip.NewReader].
|
||||
// Note that ReaderAt by definition disregards the seek position of src.
|
||||
//
|
||||
// The ReadAt method of the returned ReaderAt can be called concurrently.
|
||||
// The ReaderAt will internally cache the most recently decrypted chunk.
|
||||
// DecryptReaderAt reads and decrypts the final chunk before returning,
|
||||
// to authenticate the plaintext size.
|
||||
//
|
||||
// If no identity matches the encrypted file, the returned error will be of
|
||||
// type [NoIdentityMatchError].
|
||||
func DecryptReaderAt(src io.ReaderAt, encryptedSize int64, identities ...Identity) (io.ReaderAt, int64, error) {
|
||||
srcReader := io.NewSectionReader(src, 0, encryptedSize)
|
||||
hdr, payload, err := format.Parse(srcReader)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := hdr.Marshal(buf); err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to serialize header: %w", err)
|
||||
}
|
||||
|
||||
fileKey, err := decryptHdr(hdr, identities...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := io.ReadFull(payload, nonce); err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to read nonce: %w", err)
|
||||
}
|
||||
|
||||
payloadOffset := int64(buf.Len()) + int64(len(nonce))
|
||||
payloadSize := encryptedSize - payloadOffset
|
||||
plaintextSize, err := stream.PlaintextSize(payloadSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
payloadReaderAt := io.NewSectionReader(src, payloadOffset, payloadSize)
|
||||
r, err := stream.NewDecryptReaderAt(streamKey(fileKey, nonce), payloadReaderAt, payloadSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return r, plaintextSize, nil
|
||||
}
|
||||
|
||||
func decryptHdr(hdr *format.Header, identities ...Identity) ([]byte, error) {
|
||||
if len(identities) == 0 {
|
||||
return nil, errors.New("no identities specified")
|
||||
}
|
||||
slices.SortStableFunc(identities, func(a, b Identity) int {
|
||||
var aIsNative, bIsNative bool
|
||||
switch a.(type) {
|
||||
case *X25519Identity, *HybridIdentity, *ScryptIdentity:
|
||||
aIsNative = true
|
||||
}
|
||||
switch b.(type) {
|
||||
case *X25519Identity, *HybridIdentity, *ScryptIdentity:
|
||||
bIsNative = true
|
||||
}
|
||||
if aIsNative && !bIsNative {
|
||||
return -1
|
||||
}
|
||||
if !aIsNative && bIsNative {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
|
||||
errNoMatch := &NoIdentityMatchError{}
|
||||
for _, s := range hdr.Recipients {
|
||||
errNoMatch.StanzaTypes = append(errNoMatch.StanzaTypes, s.Type)
|
||||
stanzas = append(stanzas, (*Stanza)(s))
|
||||
}
|
||||
errNoMatch := &NoIdentityMatchError{}
|
||||
var fileKey []byte
|
||||
for _, id := range identities {
|
||||
var err error
|
||||
fileKey, err = id.Unwrap(stanzas)
|
||||
if errors.Is(err, ErrIncorrectIdentity) {
|
||||
errNoMatch.Errors = append(errNoMatch.Errors, err)
|
||||
@@ -243,12 +382,7 @@ func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
|
||||
return nil, errors.New("bad header MAC")
|
||||
}
|
||||
|
||||
nonce := make([]byte, streamNonceSize)
|
||||
if _, err := io.ReadFull(payload, nonce); err != nil {
|
||||
return nil, fmt.Errorf("failed to read nonce: %w", err)
|
||||
}
|
||||
|
||||
return stream.NewReader(streamKey(fileKey, nonce), payload)
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
// multiUnwrap is a helper that implements Identity.Unwrap in terms of a
|
||||
@@ -269,3 +403,56 @@ func multiUnwrap(unwrap func(*Stanza) ([]byte, error), stanzas []*Stanza) ([]byt
|
||||
}
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
|
||||
// ExtractHeader returns a detached header from the src file.
|
||||
//
|
||||
// The detached header can be decrypted with [DecryptHeader] (for example on a
|
||||
// different system, without sharing the ciphertext) and then the file key can
|
||||
// be used with [NewInjectedFileKeyIdentity].
|
||||
//
|
||||
// This is a low-level function that most users won't need.
|
||||
func ExtractHeader(src io.Reader) ([]byte, error) {
|
||||
hdr, _, err := format.Parse(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := hdr.Marshal(buf); err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize header: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// DecryptHeader decrypts a detached header and returns a file key.
|
||||
//
|
||||
// The detached header can be produced by [ExtractHeader], and the
|
||||
// returned file key can be used with [NewInjectedFileKeyIdentity].
|
||||
//
|
||||
// This is a low-level function that most users won't need.
|
||||
// It is the caller's responsibility to keep track of what file the
|
||||
// returned file key decrypts, and to ensure the file key is not used
|
||||
// for any other purpose.
|
||||
func DecryptHeader(header []byte, identities ...Identity) ([]byte, error) {
|
||||
hdr, _, err := format.Parse(bytes.NewReader(header))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
return decryptHdr(hdr, identities...)
|
||||
}
|
||||
|
||||
type injectedFileKeyIdentity struct {
|
||||
fileKey []byte
|
||||
}
|
||||
|
||||
// NewInjectedFileKeyIdentity returns an [Identity] that always produces
|
||||
// a fixed file key, allowing the use of a file key obtained out-of-band,
|
||||
// for example via [DecryptHeader].
|
||||
//
|
||||
// This is a low-level function that most users won't need.
|
||||
func NewInjectedFileKeyIdentity(fileKey []byte) Identity {
|
||||
return injectedFileKeyIdentity{fileKey}
|
||||
}
|
||||
|
||||
func (i injectedFileKeyIdentity) Unwrap(stanzas []*Stanza) (fileKey []byte, err error) {
|
||||
return i.fileKey, nil
|
||||
}
|
||||
+1
-1
@@ -37,7 +37,7 @@ func polymod(values []byte) uint32 {
|
||||
top := chk >> 25
|
||||
chk = (chk & 0x1ffffff) << 5
|
||||
chk = chk ^ uint32(v)
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
bit := top >> i & 1
|
||||
if bit == 1 {
|
||||
chk ^= generator[i]
|
||||
|
||||
+5
-6
@@ -77,10 +77,7 @@ func (w *WrappedBase64Encoder) writeWrapped(p []byte) (int, error) {
|
||||
panic("age: internal error: non-empty WrappedBase64Encoder.buf")
|
||||
}
|
||||
for len(p) > 0 {
|
||||
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||
if toWrite > len(p) {
|
||||
toWrite = len(p)
|
||||
}
|
||||
toWrite := min(ColumnsPerLine-(w.written%ColumnsPerLine), len(p))
|
||||
n, _ := w.buf.Write(p[:toWrite])
|
||||
w.written += n
|
||||
p = p[n:]
|
||||
@@ -228,7 +225,7 @@ func (e *ParseError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func errorf(format string, a ...interface{}) error {
|
||||
func errorf(format string, a ...any) error {
|
||||
return &ParseError{fmt.Errorf(format, a...)}
|
||||
}
|
||||
|
||||
@@ -239,7 +236,9 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
rr := bufio.NewReader(input)
|
||||
|
||||
line, err := rr.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, nil, errorf("file is empty")
|
||||
} else if err != nil {
|
||||
return nil, nil, errorf("failed to read intro: %w", err)
|
||||
}
|
||||
if line != intro {
|
||||
|
||||
+260
-39
@@ -6,17 +6,45 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const ChunkSize = 64 * 1024
|
||||
|
||||
type Reader struct {
|
||||
func EncryptedChunkCount(encryptedSize int64) (int64, error) {
|
||||
chunks := (encryptedSize + encChunkSize - 1) / encChunkSize
|
||||
|
||||
plaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead
|
||||
expChunks := (plaintextSize + ChunkSize - 1) / ChunkSize
|
||||
// Empty plaintext, the only case that allows (and requires) an empty chunk.
|
||||
if plaintextSize == 0 {
|
||||
expChunks = 1
|
||||
}
|
||||
if expChunks != chunks {
|
||||
return 0, fmt.Errorf("invalid encrypted payload size: %d", encryptedSize)
|
||||
}
|
||||
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func PlaintextSize(encryptedSize int64) (int64, error) {
|
||||
chunks, err := EncryptedChunkCount(encryptedSize)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
plaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead
|
||||
return plaintextSize, nil
|
||||
}
|
||||
|
||||
type DecryptReader struct {
|
||||
a cipher.AEAD
|
||||
src io.Reader
|
||||
|
||||
@@ -32,18 +60,15 @@ const (
|
||||
lastChunkFlag = 0x01
|
||||
)
|
||||
|
||||
func NewReader(key []byte, src io.Reader) (*Reader, error) {
|
||||
func NewDecryptReader(key []byte, src io.Reader) (*DecryptReader, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Reader{
|
||||
a: aead,
|
||||
src: src,
|
||||
}, nil
|
||||
return &DecryptReader{a: aead, src: src}, nil
|
||||
}
|
||||
|
||||
func (r *Reader) Read(p []byte) (int, error) {
|
||||
func (r *DecryptReader) Read(p []byte) (int, error) {
|
||||
if len(r.unread) > 0 {
|
||||
n := copy(p, r.unread)
|
||||
r.unread = r.unread[n:]
|
||||
@@ -85,7 +110,7 @@ func (r *Reader) Read(p []byte) (int, error) {
|
||||
// readChunk reads the next chunk of ciphertext from r.src and makes it available
|
||||
// in r.unread. last is true if the chunk was marked as the end of the message.
|
||||
// readChunk must not be called again after returning a last chunk or an error.
|
||||
func (r *Reader) readChunk() (last bool, err error) {
|
||||
func (r *DecryptReader) readChunk() (last bool, err error) {
|
||||
if len(r.unread) != 0 {
|
||||
panic("stream: internal error: readChunk called with dirty buffer")
|
||||
}
|
||||
@@ -118,7 +143,7 @@ func (r *Reader) readChunk() (last bool, err error) {
|
||||
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return false, errors.New("failed to decrypt and authenticate payload chunk")
|
||||
return false, errors.New("failed to decrypt and authenticate payload chunk, file may be corrupted or tampered with")
|
||||
}
|
||||
|
||||
incNonce(&r.nonce)
|
||||
@@ -130,12 +155,17 @@ func incNonce(nonce *[chacha20poly1305.NonceSize]byte) {
|
||||
for i := len(nonce) - 2; i >= 0; i-- {
|
||||
nonce[i]++
|
||||
if nonce[i] != 0 {
|
||||
break
|
||||
} else if i == 0 {
|
||||
// The counter is 88 bits, this is unreachable.
|
||||
panic("stream: chunk counter wrapped around")
|
||||
return
|
||||
}
|
||||
}
|
||||
// The counter is 88 bits, this is unreachable.
|
||||
panic("stream: chunk counter wrapped around")
|
||||
}
|
||||
|
||||
func nonceForChunk(chunkIndex int64) *[chacha20poly1305.NonceSize]byte {
|
||||
var nonce [chacha20poly1305.NonceSize]byte
|
||||
binary.BigEndian.PutUint64(nonce[3:11], uint64(chunkIndex))
|
||||
return &nonce
|
||||
}
|
||||
|
||||
func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {
|
||||
@@ -146,30 +176,23 @@ func nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool {
|
||||
return *nonce == [chacha20poly1305.NonceSize]byte{}
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
a cipher.AEAD
|
||||
dst io.Writer
|
||||
unwritten []byte // backed by buf
|
||||
buf [encChunkSize]byte
|
||||
nonce [chacha20poly1305.NonceSize]byte
|
||||
err error
|
||||
type EncryptWriter struct {
|
||||
a cipher.AEAD
|
||||
dst io.Writer
|
||||
buf bytes.Buffer
|
||||
nonce [chacha20poly1305.NonceSize]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func NewWriter(key []byte, dst io.Writer) (*Writer, error) {
|
||||
func NewEncryptWriter(key []byte, dst io.Writer) (*EncryptWriter, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w := &Writer{
|
||||
a: aead,
|
||||
dst: dst,
|
||||
}
|
||||
w.unwritten = w.buf[:0]
|
||||
return w, nil
|
||||
return &EncryptWriter{a: aead, dst: dst}, nil
|
||||
}
|
||||
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
// TODO: consider refactoring with a bytes.Buffer.
|
||||
func (w *EncryptWriter) Write(p []byte) (n int, err error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
@@ -179,12 +202,13 @@ func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
|
||||
total := len(p)
|
||||
for len(p) > 0 {
|
||||
freeBuf := w.buf[len(w.unwritten):ChunkSize]
|
||||
n := copy(freeBuf, p)
|
||||
n := min(len(p), ChunkSize-w.buf.Len())
|
||||
w.buf.Write(p[:n])
|
||||
p = p[n:]
|
||||
w.unwritten = w.unwritten[:len(w.unwritten)+n]
|
||||
|
||||
if len(w.unwritten) == ChunkSize && len(p) > 0 {
|
||||
// Only flush if there's a full chunk with bytes still to write, or we
|
||||
// can't know if this is the last chunk yet.
|
||||
if w.buf.Len() == ChunkSize && len(p) > 0 {
|
||||
if err := w.flushChunk(notLastChunk); err != nil {
|
||||
w.err = err
|
||||
return 0, err
|
||||
@@ -195,7 +219,7 @@ func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// Close flushes the last chunk. It does not close the underlying Writer.
|
||||
func (w *Writer) Close() error {
|
||||
func (w *EncryptWriter) Close() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
@@ -214,17 +238,214 @@ const (
|
||||
notLastChunk = false
|
||||
)
|
||||
|
||||
func (w *Writer) flushChunk(last bool) error {
|
||||
if !last && len(w.unwritten) != ChunkSize {
|
||||
func (w *EncryptWriter) flushChunk(last bool) error {
|
||||
if !last && w.buf.Len() != ChunkSize {
|
||||
panic("stream: internal error: flush called with partial chunk")
|
||||
}
|
||||
|
||||
if last {
|
||||
setLastChunkFlag(&w.nonce)
|
||||
}
|
||||
buf := w.a.Seal(w.buf[:0], w.nonce[:], w.unwritten, nil)
|
||||
_, err := w.dst.Write(buf)
|
||||
w.unwritten = w.buf[:0]
|
||||
w.buf.Grow(chacha20poly1305.Overhead)
|
||||
ciphertext := w.a.Seal(w.buf.Bytes()[:0], w.nonce[:], w.buf.Bytes(), nil)
|
||||
_, err := w.dst.Write(ciphertext)
|
||||
incNonce(&w.nonce)
|
||||
w.buf.Reset()
|
||||
return err
|
||||
}
|
||||
|
||||
type EncryptReader struct {
|
||||
a cipher.AEAD
|
||||
src io.Reader
|
||||
|
||||
// The first ready bytes of buf are already encrypted. This may be less than
|
||||
// buf.Len(), because we need to over-read to know if a chunk is the last.
|
||||
ready int
|
||||
buf bytes.Buffer
|
||||
|
||||
nonce [chacha20poly1305.NonceSize]byte
|
||||
err error
|
||||
}
|
||||
|
||||
func NewEncryptReader(key []byte, src io.Reader) (*EncryptReader, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EncryptReader{a: aead, src: src}, nil
|
||||
}
|
||||
|
||||
func (r *EncryptReader) Read(p []byte) (int, error) {
|
||||
if r.ready > 0 {
|
||||
n, err := r.buf.Read(p[:min(len(p), r.ready)])
|
||||
r.ready -= n
|
||||
return n, err
|
||||
}
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err := r.feedBuffer(); err != nil {
|
||||
r.err = err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := r.buf.Read(p[:min(len(p), r.ready)])
|
||||
r.ready -= n
|
||||
return n, err
|
||||
}
|
||||
|
||||
// feedBuffer reads and encrypts the next chunk from r.src and appends it to
|
||||
// r.buf. It sets r.ready to the number of newly available bytes in r.buf.
|
||||
func (r *EncryptReader) feedBuffer() error {
|
||||
if r.ready > 0 {
|
||||
panic("stream: internal error: feedBuffer called with dirty buffer")
|
||||
}
|
||||
|
||||
// CopyN will use r.buf.ReadFrom/WriteTo to fill the buffer directly.
|
||||
// We need ChunkSize + 1 bytes to determine if this is the last chunk.
|
||||
_, err := io.CopyN(&r.buf, r.src, int64(ChunkSize-r.buf.Len()+1))
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
if last := r.buf.Len() <= ChunkSize; last {
|
||||
setLastChunkFlag(&r.nonce)
|
||||
|
||||
// After Grow, we know r.buf.Bytes() has enough capacity for the
|
||||
// overhead. We encrypt in place and then do a Write to include the
|
||||
// overhead in the buffer.
|
||||
r.buf.Grow(chacha20poly1305.Overhead)
|
||||
plaintext := r.buf.Bytes()
|
||||
r.a.Seal(plaintext[:0], r.nonce[:], plaintext, nil)
|
||||
incNonce(&r.nonce)
|
||||
r.buf.Write(plaintext[len(plaintext) : len(plaintext)+chacha20poly1305.Overhead])
|
||||
r.ready = r.buf.Len()
|
||||
|
||||
r.err = io.EOF
|
||||
return nil
|
||||
}
|
||||
|
||||
// Same, but accounting for the tail byte which will remain unencrypted and
|
||||
// needs to be shifted past the overhead.
|
||||
if r.buf.Len() != ChunkSize+1 {
|
||||
panic("stream: internal error: unexpected buffer length")
|
||||
}
|
||||
tailByte := r.buf.Bytes()[ChunkSize]
|
||||
r.buf.Grow(chacha20poly1305.Overhead)
|
||||
plaintext := r.buf.Bytes()[:ChunkSize]
|
||||
r.a.Seal(plaintext[:0], r.nonce[:], plaintext, nil)
|
||||
incNonce(&r.nonce)
|
||||
r.buf.Write(plaintext[len(plaintext)+1 : len(plaintext)+chacha20poly1305.Overhead])
|
||||
r.buf.WriteByte(tailByte)
|
||||
r.ready = ChunkSize + chacha20poly1305.Overhead
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DecryptReaderAt struct {
|
||||
a cipher.AEAD
|
||||
src io.ReaderAt
|
||||
size int64
|
||||
chunks int64
|
||||
cache atomic.Pointer[cachedChunk]
|
||||
}
|
||||
|
||||
type cachedChunk struct {
|
||||
off int64
|
||||
data []byte
|
||||
}
|
||||
|
||||
func NewDecryptReaderAt(key []byte, src io.ReaderAt, size int64) (*DecryptReaderAt, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check that size is valid by decrypting the final chunk.
|
||||
chunks, err := EncryptedChunkCount(size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
finalChunkIndex := chunks - 1
|
||||
finalChunkOff := finalChunkIndex * encChunkSize
|
||||
finalChunkSize := size - finalChunkOff
|
||||
finalChunk := make([]byte, finalChunkSize)
|
||||
if _, err := src.ReadAt(finalChunk, finalChunkOff); err != nil {
|
||||
return nil, fmt.Errorf("failed to read final chunk: %w", err)
|
||||
}
|
||||
nonce := nonceForChunk(finalChunkIndex)
|
||||
setLastChunkFlag(nonce)
|
||||
plaintext, err := aead.Open(finalChunk[:0], nonce[:], finalChunk, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt and authenticate final chunk: %w", err)
|
||||
}
|
||||
cache := &cachedChunk{off: finalChunkOff, data: plaintext}
|
||||
|
||||
plaintextSize := size - chunks*chacha20poly1305.Overhead
|
||||
r := &DecryptReaderAt{a: aead, src: src, size: plaintextSize, chunks: chunks}
|
||||
r.cache.Store(cache)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *DecryptReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
if off < 0 || off > r.size {
|
||||
return 0, fmt.Errorf("offset out of range [0:%d]: %d", r.size, off)
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var cacheUpdate *cachedChunk
|
||||
chunk := make([]byte, encChunkSize)
|
||||
for len(p) > 0 && off < r.size {
|
||||
chunkIndex := off / ChunkSize
|
||||
chunkOff := chunkIndex * encChunkSize
|
||||
encSize := r.size + r.chunks*chacha20poly1305.Overhead
|
||||
chunkSize := min(encSize-chunkOff, encChunkSize)
|
||||
|
||||
cached := r.cache.Load()
|
||||
var plaintext []byte
|
||||
if cached != nil && cached.off == chunkOff {
|
||||
plaintext = cached.data
|
||||
cacheUpdate = nil
|
||||
} else {
|
||||
nn, err := r.src.ReadAt(chunk[:chunkSize], chunkOff)
|
||||
if err == io.EOF {
|
||||
if int64(nn) != chunkSize {
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to read chunk at offset %d: %w", chunkOff, err)
|
||||
}
|
||||
nonce := nonceForChunk(chunkIndex)
|
||||
if chunkIndex == r.chunks-1 {
|
||||
setLastChunkFlag(nonce)
|
||||
}
|
||||
plaintext, err = r.a.Open(chunk[:0], nonce[:], chunk[:chunkSize], nil)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to decrypt and authenticate chunk at offset %d: %w", chunkOff, err)
|
||||
}
|
||||
cacheUpdate = &cachedChunk{off: chunkOff, data: plaintext}
|
||||
}
|
||||
|
||||
plainChunkOff := int(off - chunkIndex*ChunkSize)
|
||||
copySize := min(len(plaintext)-plainChunkOff, len(p))
|
||||
copy(p, plaintext[plainChunkOff:plainChunkOff+copySize])
|
||||
p = p[copySize:]
|
||||
off += int64(copySize)
|
||||
n += copySize
|
||||
}
|
||||
if cacheUpdate != nil {
|
||||
r.cache.Store(cacheUpdate)
|
||||
}
|
||||
if off == r.size {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
+41
-13
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ParseIdentities parses a file with one or more private key encodings, one per
|
||||
@@ -16,10 +17,10 @@ import (
|
||||
//
|
||||
// This is the same syntax as the private key files accepted by the CLI, except
|
||||
// the CLI also accepts SSH private keys, which are not recommended for the
|
||||
// average application.
|
||||
// average application, and plugins, which involve invoking external programs.
|
||||
//
|
||||
// Currently, all returned values are of type *X25519Identity, but different
|
||||
// types might be returned in the future.
|
||||
// Currently, all returned values are of type *[X25519Identity] or
|
||||
// *[HybridIdentity], but different types might be returned in the future.
|
||||
func ParseIdentities(f io.Reader) ([]Identity, error) {
|
||||
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||
var ids []Identity
|
||||
@@ -31,30 +32,45 @@ func ParseIdentities(f io.Reader) ([]Identity, error) {
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
i, err := ParseX25519Identity(line)
|
||||
if !utf8.ValidString(line) {
|
||||
return nil, fmt.Errorf("identities file is not valid UTF-8")
|
||||
}
|
||||
i, err := parseIdentity(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error at line %d: %v", n, err)
|
||||
}
|
||||
ids = append(ids, i)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret keys file: %v", err)
|
||||
return nil, fmt.Errorf("failed to read identities file: %v", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("no secret keys found")
|
||||
return nil, fmt.Errorf("no identities found")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func parseIdentity(arg string) (Identity, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(arg, "AGE-SECRET-KEY-1"):
|
||||
return ParseX25519Identity(arg)
|
||||
case strings.HasPrefix(arg, "AGE-SECRET-KEY-PQ-1"):
|
||||
return ParseHybridIdentity(arg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown identity type: %q", arg)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRecipients parses a file with one or more public key encodings, one per
|
||||
// line. Empty lines and lines starting with "#" are ignored.
|
||||
//
|
||||
// This is the same syntax as the recipients files accepted by the CLI, except
|
||||
// the CLI also accepts SSH recipients, which are not recommended for the
|
||||
// average application.
|
||||
// average application, tagged recipients, which have different privacy
|
||||
// properties, and plugins, which involve invoking external programs.
|
||||
//
|
||||
// Currently, all returned values are of type *X25519Recipient, but different
|
||||
// types might be returned in the future.
|
||||
// Currently, all returned values are of type *[X25519Recipient] or
|
||||
// *[HybridRecipient] but different types might be returned in the future.
|
||||
func ParseRecipients(f io.Reader) ([]Recipient, error) {
|
||||
const recipientFileSizeLimit = 1 << 24 // 16 MiB
|
||||
var recs []Recipient
|
||||
@@ -66,11 +82,12 @@ func ParseRecipients(f io.Reader) ([]Recipient, error) {
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
r, err := ParseX25519Recipient(line)
|
||||
if !utf8.ValidString(line) {
|
||||
return nil, fmt.Errorf("recipients file is not valid UTF-8")
|
||||
}
|
||||
r, err := parseRecipient(line)
|
||||
if err != nil {
|
||||
// Hide the error since it might unintentionally leak the contents
|
||||
// of confidential files.
|
||||
return nil, fmt.Errorf("malformed recipient at line %d", n)
|
||||
return nil, fmt.Errorf("error at line %d: %v", n, err)
|
||||
}
|
||||
recs = append(recs, r)
|
||||
}
|
||||
@@ -82,3 +99,14 @@ func ParseRecipients(f io.Reader) ([]Recipient, error) {
|
||||
}
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func parseRecipient(arg string) (Recipient, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(arg, "age1pq1"):
|
||||
return ParseHybridRecipient(arg)
|
||||
case strings.HasPrefix(arg, "age1"):
|
||||
return ParseX25519Recipient(arg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown recipient type: %q", arg)
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
// Copyright 2025 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/bech32"
|
||||
"filippo.io/age/internal/format"
|
||||
"filippo.io/hpke"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const pqLabel = "age-encryption.org/mlkem768x25519"
|
||||
|
||||
// HybridRecipient is the standard age public key. Messages encrypted to
|
||||
// this recipient can be decrypted with the corresponding [HybridIdentity].
|
||||
//
|
||||
// This recipient is safe against future cryptographically-relevant quantum
|
||||
// computers, and can only be used along with other post-quantum recipients.
|
||||
//
|
||||
// This recipient is anonymous, in the sense that an attacker can't tell from
|
||||
// the message alone if it is encrypted to a certain recipient.
|
||||
type HybridRecipient struct {
|
||||
pk hpke.PublicKey
|
||||
}
|
||||
|
||||
var _ Recipient = &HybridRecipient{}
|
||||
|
||||
// newHybridRecipient returns a new [HybridRecipient] from a raw HPKE public key.
|
||||
func newHybridRecipient(publicKey []byte) (*HybridRecipient, error) {
|
||||
pk, err := hpke.MLKEM768X25519().NewPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid MLKEM768-X25519 public key")
|
||||
}
|
||||
return &HybridRecipient{pk: pk}, nil
|
||||
}
|
||||
|
||||
// ParseHybridRecipient returns a new [HybridRecipient] from a Bech32 public key
|
||||
// encoding with the "age1pq1" prefix.
|
||||
func ParseHybridRecipient(s string) (*HybridRecipient, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
||||
}
|
||||
if t != "age1pq" {
|
||||
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
|
||||
}
|
||||
r, err := newHybridRecipient(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *HybridRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
|
||||
s, _, err := r.WrapWithLabels(fileKey)
|
||||
return s, err
|
||||
}
|
||||
|
||||
// WrapWithLabels implements [RecipientWithLabels], returning a single
|
||||
// "postquantum" label. This ensures a HybridRecipient can't be mixed with other
|
||||
// recipients that would defeat its post-quantum security.
|
||||
//
|
||||
// To unsafely bypass this restriction, wrap HybridRecipient in a [Recipient]
|
||||
// type that doesn't expose WrapWithLabels.
|
||||
func (r *HybridRecipient) WrapWithLabels(fileKey []byte) ([]*Stanza, []string, error) {
|
||||
enc, s, err := hpke.NewSender(r.pk, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(pqLabel))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to set up HPKE sender: %v", err)
|
||||
}
|
||||
ct, err := s.Seal(nil, fileKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encrypt file key: %v", err)
|
||||
}
|
||||
|
||||
l := &Stanza{
|
||||
Type: "mlkem768x25519",
|
||||
Args: []string{format.EncodeToString(enc)},
|
||||
Body: ct,
|
||||
}
|
||||
|
||||
return []*Stanza{l}, []string{"postquantum"}, nil
|
||||
}
|
||||
|
||||
// String returns the Bech32 public key encoding of r.
|
||||
func (r *HybridRecipient) String() string {
|
||||
s, _ := bech32.Encode("age1pq", r.pk.Bytes())
|
||||
return s
|
||||
}
|
||||
|
||||
// HybridIdentity is the standard age private key, which can decrypt messages
|
||||
// encrypted to the corresponding [HybridRecipient].
|
||||
type HybridIdentity struct {
|
||||
k hpke.PrivateKey
|
||||
}
|
||||
|
||||
var _ Identity = &HybridIdentity{}
|
||||
|
||||
// newHybridIdentity returns a new [HybridIdentity] from a raw HPKE private key.
|
||||
func newHybridIdentity(secretKey []byte) (*HybridIdentity, error) {
|
||||
k, err := hpke.MLKEM768X25519().NewPrivateKey(secretKey)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid MLKEM768-X25519 secret key")
|
||||
}
|
||||
return &HybridIdentity{k: k}, nil
|
||||
}
|
||||
|
||||
// GenerateHybridIdentity randomly generates a new [HybridIdentity].
|
||||
func GenerateHybridIdentity() (*HybridIdentity, error) {
|
||||
k, err := hpke.MLKEM768X25519().GenerateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate post-quantum identity: %v", err)
|
||||
}
|
||||
return &HybridIdentity{k: k}, nil
|
||||
}
|
||||
|
||||
// ParseHybridIdentity returns a new [HybridIdentity] from a Bech32 private key
|
||||
// encoding with the "AGE-SECRET-KEY-PQ-1" prefix.
|
||||
func ParseHybridIdentity(s string) (*HybridIdentity, error) {
|
||||
t, k, err := bech32.Decode(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
if t != "AGE-SECRET-KEY-PQ-" {
|
||||
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
|
||||
}
|
||||
r, err := newHybridIdentity(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed secret key: %v", err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (i *HybridIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
func (i *HybridIdentity) unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "mlkem768x25519" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
if len(block.Args) != 1 {
|
||||
return nil, errors.New("invalid mlkem768x25519 recipient block")
|
||||
}
|
||||
enc, err := format.DecodeString(block.Args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse mlkem768x25519 recipient: %v", err)
|
||||
}
|
||||
if len(block.Body) != fileKeySize+chacha20poly1305.Overhead {
|
||||
return nil, errIncorrectCiphertextSize
|
||||
}
|
||||
|
||||
r, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(pqLabel))
|
||||
if err != nil {
|
||||
// MLKEM768-X25519 does implicit rejection, so a mismatched key does not
|
||||
// hit this error path, but is only detected later when trying to open.
|
||||
return nil, fmt.Errorf("invalid mlkem768x25519 recipient: %v", err)
|
||||
}
|
||||
fileKey, err := r.Open(nil, block.Body)
|
||||
if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
|
||||
// Recipient returns the public [HybridRecipient] value corresponding to i.
|
||||
func (i *HybridIdentity) Recipient() *HybridRecipient {
|
||||
return &HybridRecipient{pk: i.k.PublicKey()}
|
||||
}
|
||||
|
||||
// String returns the Bech32 private key encoding of i.
|
||||
func (i *HybridIdentity) String() string {
|
||||
b, _ := i.k.Bytes()
|
||||
s, _ := bech32.Encode("AGE-SECRET-KEY-PQ-", b)
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
+12
-4
@@ -27,7 +27,7 @@ const scryptLabel = "age-encryption.org/v1/scrypt"
|
||||
// for the same file.
|
||||
//
|
||||
// Its use is not recommended for automated systems, which should prefer
|
||||
// X25519Recipient.
|
||||
// [HybridRecipient] or [X25519Recipient].
|
||||
type ScryptRecipient struct {
|
||||
password []byte
|
||||
workFactor int
|
||||
@@ -150,14 +150,20 @@ func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
for _, s := range stanzas {
|
||||
if s.Type != "scrypt" {
|
||||
continue
|
||||
}
|
||||
return i.unwrap(s)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: file is not passphrase-encrypted", ErrIncorrectIdentity)
|
||||
}
|
||||
|
||||
var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)
|
||||
|
||||
func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
|
||||
if block.Type != "scrypt" {
|
||||
return nil, ErrIncorrectIdentity
|
||||
return nil, errors.New("internal error: unwrap called on non-scrypt stanza")
|
||||
}
|
||||
if len(block.Args) != 2 {
|
||||
return nil, errors.New("invalid scrypt recipient block")
|
||||
@@ -200,7 +206,9 @@ func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
|
||||
if err == errIncorrectCiphertextSize {
|
||||
return nil, errors.New("invalid scrypt recipient block: incorrect file key size")
|
||||
} else if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
// Wrap [ErrIncorrectIdentity] so that multiple passphrases can be tried
|
||||
// in sequence by passing multiple [ScryptIdentity] values to [Decrypt].
|
||||
return nil, fmt.Errorf("%w: incorrect passphrase", ErrIncorrectIdentity)
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
+6
-4
@@ -21,8 +21,9 @@ import (
|
||||
|
||||
const x25519Label = "age-encryption.org/v1/X25519"
|
||||
|
||||
// X25519Recipient is the standard age public key. Messages encrypted to this
|
||||
// recipient can be decrypted with the corresponding X25519Identity.
|
||||
// X25519Recipient is the standard age pre-quantum public key. Messages
|
||||
// encrypted to this recipient can be decrypted with the corresponding
|
||||
// [X25519Identity]. For post-quantum resistance, use [HybridRecipient].
|
||||
//
|
||||
// This recipient is anonymous, in the sense that an attacker can't tell from
|
||||
// the message alone if it is encrypted to a certain recipient.
|
||||
@@ -105,8 +106,9 @@ func (r *X25519Recipient) String() string {
|
||||
return s
|
||||
}
|
||||
|
||||
// X25519Identity is the standard age private key, which can decrypt messages
|
||||
// encrypted to the corresponding X25519Recipient.
|
||||
// X25519Identity is the standard pre-quantum age private key, which can decrypt
|
||||
// messages encrypted to the corresponding [X25519Recipient]. For post-quantum
|
||||
// resistance, use [HybridIdentity].
|
||||
type X25519Identity struct {
|
||||
secretKey, ourPublicKey []byte
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package hpke
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// The AEAD is one of the three components of an HPKE ciphersuite, implementing
|
||||
// symmetric encryption.
|
||||
type AEAD interface {
|
||||
ID() uint16
|
||||
keySize() int
|
||||
nonceSize() int
|
||||
aead(key []byte) (cipher.AEAD, error)
|
||||
}
|
||||
|
||||
// NewAEAD returns the AEAD implementation for the given AEAD ID.
|
||||
//
|
||||
// Applications are encouraged to use specific implementations like [AES128GCM]
|
||||
// or [ChaCha20Poly1305] instead, unless runtime agility is required.
|
||||
func NewAEAD(id uint16) (AEAD, error) {
|
||||
switch id {
|
||||
case 0x0001: // AES-128-GCM
|
||||
return AES128GCM(), nil
|
||||
case 0x0002: // AES-256-GCM
|
||||
return AES256GCM(), nil
|
||||
case 0x0003: // ChaCha20Poly1305
|
||||
return ChaCha20Poly1305(), nil
|
||||
case 0xFFFF: // Export-only
|
||||
return ExportOnly(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported AEAD %04x", id)
|
||||
}
|
||||
}
|
||||
|
||||
// AES128GCM returns an AES-128-GCM AEAD implementation.
|
||||
func AES128GCM() AEAD { return aes128GCM }
|
||||
|
||||
// AES256GCM returns an AES-256-GCM AEAD implementation.
|
||||
func AES256GCM() AEAD { return aes256GCM }
|
||||
|
||||
// ChaCha20Poly1305 returns a ChaCha20Poly1305 AEAD implementation.
|
||||
func ChaCha20Poly1305() AEAD { return chacha20poly1305AEAD }
|
||||
|
||||
// ExportOnly returns a placeholder AEAD implementation that cannot encrypt or
|
||||
// decrypt, but only export secrets with [Sender.Export] or [Recipient.Export].
|
||||
//
|
||||
// When this is used, [Sender.Seal] and [Recipient.Open] return errors.
|
||||
func ExportOnly() AEAD { return exportOnlyAEAD{} }
|
||||
|
||||
type aead struct {
|
||||
nK int
|
||||
nN int
|
||||
new func([]byte) (cipher.AEAD, error)
|
||||
id uint16
|
||||
}
|
||||
|
||||
var aes128GCM = &aead{
|
||||
nK: 128 / 8,
|
||||
nN: 96 / 8,
|
||||
new: newAESGCM,
|
||||
id: 0x0001,
|
||||
}
|
||||
|
||||
var aes256GCM = &aead{
|
||||
nK: 256 / 8,
|
||||
nN: 96 / 8,
|
||||
new: newAESGCM,
|
||||
id: 0x0002,
|
||||
}
|
||||
|
||||
var chacha20poly1305AEAD = &aead{
|
||||
nK: chacha20poly1305.KeySize,
|
||||
nN: chacha20poly1305.NonceSize,
|
||||
new: chacha20poly1305.New,
|
||||
id: 0x0003,
|
||||
}
|
||||
|
||||
func newAESGCM(key []byte) (cipher.AEAD, error) {
|
||||
b, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewGCM(b)
|
||||
}
|
||||
|
||||
func (a *aead) ID() uint16 {
|
||||
return a.id
|
||||
}
|
||||
|
||||
func (a *aead) aead(key []byte) (cipher.AEAD, error) {
|
||||
if len(key) != a.nK {
|
||||
return nil, errors.New("invalid key size")
|
||||
}
|
||||
return a.new(key)
|
||||
}
|
||||
|
||||
func (a *aead) keySize() int {
|
||||
return a.nK
|
||||
}
|
||||
|
||||
func (a *aead) nonceSize() int {
|
||||
return a.nN
|
||||
}
|
||||
|
||||
type exportOnlyAEAD struct{}
|
||||
|
||||
func (exportOnlyAEAD) ID() uint16 {
|
||||
return 0xFFFF
|
||||
}
|
||||
|
||||
func (exportOnlyAEAD) aead(key []byte) (cipher.AEAD, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (exportOnlyAEAD) keySize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (exportOnlyAEAD) nonceSize() int {
|
||||
return 0
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Package ecdh defines an additional interface that will be added to the
|
||||
// crypto/ecdh package in Go 1.26+.
|
||||
package ecdh
|
||||
|
||||
import "crypto/ecdh"
|
||||
|
||||
// KeyExchanger is an interface for an opaque private key that can be used for
|
||||
// key exchange operations. For example, an ECDH key kept in a hardware module.
|
||||
//
|
||||
// It is implemented by [ecdh.PrivateKey].
|
||||
type KeyExchanger interface {
|
||||
PublicKey() *ecdh.PublicKey
|
||||
Curve() ecdh.Curve
|
||||
ECDH(*ecdh.PublicKey) ([]byte, error)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ecdh
|
||||
|
||||
import "crypto/ecdh"
|
||||
|
||||
// This file contains stubs to allow importing only this package instead of
|
||||
// crypto/ecdh, to minimize the diff.
|
||||
|
||||
type Curve = ecdh.Curve
|
||||
type PrivateKey = ecdh.PrivateKey
|
||||
type PublicKey = ecdh.PublicKey
|
||||
|
||||
func X25519() Curve { return ecdh.X25519() }
|
||||
func P256() Curve { return ecdh.P256() }
|
||||
func P384() Curve { return ecdh.P384() }
|
||||
func P521() Curve { return ecdh.P521() }
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Package crypto defines additional interfaces that will be added to the
|
||||
// crypto package in Go 1.26+.
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/mlkem"
|
||||
)
|
||||
|
||||
// KeyExchanger is an interface for an opaque private key that can be used for
|
||||
// key exchange operations. For example, an ECDH key kept in a hardware module.
|
||||
//
|
||||
// It is implemented by [ecdh.PrivateKey].
|
||||
type KeyExchanger interface {
|
||||
PublicKey() *ecdh.PublicKey
|
||||
Curve() ecdh.Curve
|
||||
ECDH(*ecdh.PublicKey) ([]byte, error)
|
||||
}
|
||||
|
||||
// Encapsulator is an interface for a public KEM key that can be used for
|
||||
// encapsulation operations.
|
||||
//
|
||||
// It is implemented, for example, by [crypto/mlkem.EncapsulationKey768].
|
||||
type Encapsulator interface {
|
||||
Bytes() []byte
|
||||
Encapsulate() (sharedKey, ciphertext []byte)
|
||||
}
|
||||
|
||||
// Decapsulator is an interface for an opaque private KEM key that can be used for
|
||||
// decapsulation operations. For example, an ML-KEM key kept in a hardware module.
|
||||
//
|
||||
// It will be implemented by [crypto/mlkem.DecapsulationKey768] in Go 1.26+.
|
||||
// In the meantime, use [DecapsulatorFromDecapsulationKey768] and
|
||||
// [DecapsulatorFromDecapsulationKey1024].
|
||||
type Decapsulator interface {
|
||||
Encapsulator() Encapsulator
|
||||
Decapsulate(ciphertext []byte) (sharedKey []byte, err error)
|
||||
}
|
||||
|
||||
// DecapsulatorFromDecapsulationKey768 wraps an ML-KEM-768 decapsulation key
|
||||
// into a [Decapsulator], until Go 1.26+ where [crypto/mlkem.DecapsulationKey768]
|
||||
// implements it natively.
|
||||
func DecapsulatorFromDecapsulationKey768(dk *mlkem.DecapsulationKey768) Decapsulator {
|
||||
return &mlkem768Decapsulator{dk}
|
||||
}
|
||||
|
||||
type mlkem768Decapsulator struct {
|
||||
*mlkem.DecapsulationKey768
|
||||
}
|
||||
|
||||
func (d *mlkem768Decapsulator) Encapsulator() Encapsulator {
|
||||
return d.EncapsulationKey()
|
||||
}
|
||||
|
||||
// DecapsulatorFromDecapsulationKey1024 wraps an ML-KEM-1024 decapsulation key
|
||||
// into a [Decapsulator], until Go 1.26+ where [crypto/mlkem.DecapsulationKey1024]
|
||||
// implements it natively.
|
||||
func DecapsulatorFromDecapsulationKey1024(dk *mlkem.DecapsulationKey1024) Decapsulator {
|
||||
return &mlkem1024Decapsulator{dk}
|
||||
}
|
||||
|
||||
type mlkem1024Decapsulator struct {
|
||||
*mlkem.DecapsulationKey1024
|
||||
}
|
||||
|
||||
func (d *mlkem1024Decapsulator) Encapsulator() Encapsulator {
|
||||
return d.EncapsulationKey()
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
# HPKE Hybrid KEMs
|
||||
|
||||
[filippo.io/hpke-pq](https://filippo.io/hpke-pq)
|
||||
|
||||
This document is a simplified and self-contained implementation reference for
|
||||
the MLKEM768-X25519, MLKEM768-P256, and MLKEM1024-P384 hybrid HPKE KEMs,
|
||||
specified in [draft-ietf-hpke-pq-03][], [draft-irtf-cfrg-hybrid-kems-07][],
|
||||
[draft-irtf-cfrg-concrete-hybrid-kems-02][], and [draft-ietf-hpke-hpke-02].
|
||||
|
||||
It compensates for the need to cross-reference four documents, with different
|
||||
nomenclature (including functions and components with the same name but
|
||||
different behavior), alternative irrelevant definitions (the UG, UK, and CK
|
||||
frameworks), and multiple KEM abstraction layers.
|
||||
|
||||
## Conventions used in this document
|
||||
|
||||
`||` denotes concatenation. `[N:M]` denotes the byte slice from index N (inclusive)
|
||||
to index M (exclusive). Strings quoted with `""` are encoded as ASCII. Values in
|
||||
code blocks are hex encoded byte strings. `random(N)` denotes N bytes of CSPRNG
|
||||
output. All lengths are in bytes.
|
||||
|
||||
ML-KEM.KeyGen_internal, ML-KEM.Encaps, and ML-KEM.Decaps are defined in
|
||||
[FIPS 203][]. `SHAKE256(s, L)` is an invocation of `SHAKE256(s, 8*L)` defined in
|
||||
[FIPS 202][]. SHA3-256 is defined in [FIPS 202][].
|
||||
|
||||
## KEM definitions
|
||||
|
||||
| Parameter | MLKEM768-X25519 | MLKEM768-P256 | MLKEM1024-P384 |
|
||||
| ----------------- | ------------------ | ------------------ | ------------------ |
|
||||
| ML-KEM parameters | ML-KEM-768 | ML-KEM-768 | ML-KEM-1024 |
|
||||
| Group | Curve25519 | P-256 | P-384 |
|
||||
| KEM identifier | 0x647a | 0x0050 | 0x0051 |
|
||||
| Nsecret | 32 | 32 | 32 |
|
||||
| Nenc | 1120 | 1153 | 1665 |
|
||||
| Npk | 1216 | 1249 | 1665 |
|
||||
| Nsk | 32 | 32 | 32 |
|
||||
| Label | `"\.//^\"` | `"MLKEM768-P256"` | `"MLKEM1024-P384"` |
|
||||
| KEM.Nct | 1088 | 1088 | 1568 |
|
||||
| KEM.Nek | 1184 | 1184 | 1568 |
|
||||
| KEM.Nseed | 64 | 64 | 64 |
|
||||
| Group.Nelem | 32 | 65 | 97 |
|
||||
| Group.Nseed | 32 | 128 | 48 |
|
||||
| Group.Nscalar | N/A | 32 | 48 |
|
||||
|
||||
The MLKEM768-X25519 Label is alternatively encoded as
|
||||
|
||||
5c2e2f2f5e5c
|
||||
|
||||
## KEM functions
|
||||
|
||||
```
|
||||
def GenerateKeyPair():
|
||||
seed = random(32)
|
||||
|
||||
ek_PQ, ek_T, _, _ = expandKey(seed)
|
||||
ek = ek_PQ || ek_T
|
||||
|
||||
return (seed, ek)
|
||||
|
||||
def DeriveKeyPair(ikm):
|
||||
# SHAKE256.LabeledDerive is part of the single-stage KDF described in
|
||||
# draft-ietf-hpke-hpke-02 and defined in draft-ietf-hpke-pq-03, but is
|
||||
# reproduced below for convenience.
|
||||
seed = SHAKE256.LabeledDerive(ikm, "DeriveKeyPair", "", 32)
|
||||
|
||||
ek_PQ, ek_T, _, _ = expandKey(seed)
|
||||
ek = ek_PQ || ek_T
|
||||
|
||||
return (seed, ek)
|
||||
|
||||
def Encaps(ek):
|
||||
ek_PQ = ek[0 : KEM.Nek]
|
||||
ek_T = ek[KEM.Nek : KEM.Nek + Group.Nelem]
|
||||
|
||||
ss_PQ, ct_PQ = ML-KEM.Encaps(ek_PQ)
|
||||
|
||||
sk_E = Group.RandomScalar(random(Group.Nseed))
|
||||
ct_T = Group.Exp(Group.G, sk_E)
|
||||
ss_T = Group.ElementToSharedSecret(Group.Exp(ek_T, sk_E))
|
||||
|
||||
ss = SHA3-256(ss_PQ || ss_T || ct_T || ek_T || Label)
|
||||
ct = ct_PQ || ct_T
|
||||
|
||||
return (ss, ct)
|
||||
|
||||
def Decaps(seed, ct):
|
||||
ct_PQ = ct[0 : KEM.Nct]
|
||||
ct_T = ct[KEM.Nct : KEM.Nct + Group.Nelem]
|
||||
|
||||
ek_PQ, ek_T, dk_PQ, dk_T = expandKey(seed)
|
||||
|
||||
ss_PQ = ML-KEM.Decaps(dk_PQ, ct_PQ)
|
||||
ss_T = Group.ElementToSharedSecret(Group.Exp(ct_T, dk_T))
|
||||
|
||||
ss = SHA3-256(ss_PQ || ss_T || ct_T || ek_T || Label)
|
||||
|
||||
return ss
|
||||
|
||||
def expandKey(seed):
|
||||
seed_full = SHAKE256(seed, KEM.Nseed + Group.Nseed)
|
||||
seed_PQ = seed_full[0 : KEM.Nseed]
|
||||
seed_T = seed_full[KEM.Nseed : KEM.Nseed + Group.Nseed]
|
||||
|
||||
# Note that even if expandKey returns the semi-expanded ML-KEM decapsulation
|
||||
# key dk_PQ to use FIPS 203 definitions, that format should be avoided and
|
||||
# instead seed_PQ should be expanded directly into the implementation's
|
||||
# internal ML-KEM private representation.
|
||||
(ek_PQ, dk_PQ) = ML-KEM.KeyGen_internal(seed_PQ)
|
||||
dk_T = Group.RandomScalar(seed_T)
|
||||
ek_T = Group.Exp(Group.G, dk_T)
|
||||
|
||||
return (ek_PQ, ek_T, dk_PQ, dk_T)
|
||||
```
|
||||
|
||||
There is no distinction between a private/public key and its serialization:
|
||||
there is no abstract key format, only byte strings. In practice, implementations
|
||||
will probably want to load keys into pairs of internal representations, and
|
||||
serialize them back to their byte string format when needed.
|
||||
|
||||
The IETF/IRTF documents lack a specified way to turn a private key into public
|
||||
key, although it can be inferred from the key generation process. We define such
|
||||
a process here as `PrivateKeyToPublicKey`.
|
||||
|
||||
```
|
||||
def PrivateKeyToPublicKey(seed):
|
||||
ek_PQ, ek_T, _, _ = expandKey(seed)
|
||||
ek = ek_PQ || ek_T
|
||||
|
||||
return ek
|
||||
```
|
||||
|
||||
## Group definitions
|
||||
|
||||
### Curve25519
|
||||
|
||||
Group.Exp is the X25519 function defined in [RFC 7748][].
|
||||
|
||||
Group.G is the canonical generator, which encodes to
|
||||
|
||||
0900000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
consistently with [RFC 7748, Section 4.1][] and [RFC 7748, Section 6.1][].
|
||||
|
||||
Group.RandomScalar and Group.ElementToSharedSecret are the identity.
|
||||
|
||||
### P-256 and P-384
|
||||
|
||||
The NIST P-256 and P-384 elliptic curves are defined in [SP800-186][].
|
||||
|
||||
`Group.Exp(p, x)` computes scalar multiplication between the input element p and
|
||||
the scalar x. The input element p and the output element have length Group.Nelem
|
||||
and are encoded in uncompressed representation using the
|
||||
Elliptic-Curve-Point-to-Octet-String and Octet-String-to-Elliptic-Curve-Point
|
||||
functions defined in [SEC 1, Version 2.0][]. The input scalar x has length
|
||||
Group.Nscalar and is encoded in big-endian representation using the I2OSP and
|
||||
OS2IP functions defined in [RFC 8017][].
|
||||
|
||||
Group.G is the canonical generator, which encodes to
|
||||
|
||||
046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c29
|
||||
64fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5
|
||||
|
||||
for P-256, and to
|
||||
|
||||
04aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab
|
||||
73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f
|
||||
|
||||
for P-384, consistently with [SP800-186][], Section 3.2.1.
|
||||
|
||||
```
|
||||
def RandomScalar(seed):
|
||||
start = 0
|
||||
end = Nscalar
|
||||
sk = seed[start : end]
|
||||
while OS2IP(sk) == 0 || OS2IP(sk) >= OS2IP(Group.N):
|
||||
start = end
|
||||
end = end + Nscalar
|
||||
if end > len(seed):
|
||||
# This happens with cryptographically negligible probability.
|
||||
# The chance of a single rejection is < 2^-32 for P-256 and
|
||||
# < 2^-192 for P-384. The chance of reaching this is thus
|
||||
# < 2^-128 for P-256 and < 2^-192 for P-384.
|
||||
raise Exception("Rejection sampling failed")
|
||||
sk = seed[start : end]
|
||||
return sk
|
||||
```
|
||||
|
||||
Group.N is the order of the curve's group, which encodes to
|
||||
|
||||
ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
|
||||
|
||||
for P-256, and to
|
||||
|
||||
ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973
|
||||
|
||||
for P-384, consistently with [SP800-186][], Section 3.2.1.
|
||||
|
||||
Group.ElementToSharedSecret encodes the input element as an X coordinate using
|
||||
the Field-Element-to-Octet-String function in [SEC 1, Version 2.0][].
|
||||
|
||||
> Note that since the scalar x is always derived uniformly at random, the chance
|
||||
> of it being zero are cryptographically negligible. Moreover,
|
||||
> Octet-String-to-Elliptic-Curve-Point never decodes the point at infinity from
|
||||
> a string of Group.Nelem bytes. Since NIST P curves have prime order, this
|
||||
> means that the output of Group.Exp and input to Group.ElementToSharedSecret is
|
||||
> also never the point at infinity.
|
||||
|
||||
## SHAKE256.LabeledDerive
|
||||
|
||||
SHAKE256.LabeledDerive is used by DeriveKeyPair, and is part of the single-stage
|
||||
KDF specified across [draft-ietf-hpke-hpke-02][] and [draft-ietf-hpke-pq-03][],
|
||||
but is reproduced below for convenience.
|
||||
|
||||
```
|
||||
def SHAKE256.LabeledDerive(ikm, label, context, L):
|
||||
suite_id = concat("KEM", I2OSP(kem_id, 2))
|
||||
prefixed_label = I2OSP(len(label), 2) || label
|
||||
labeled_ikm = ikm || "HPKE-v1" || suite_id || prefixed_label || I2OSP(L, 2) || context
|
||||
return SHAKE256(labeled_ikm, L)
|
||||
```
|
||||
|
||||
I2OSP is defined in [RFC 8017][], and `kem_id` is the KEM identifier.
|
||||
|
||||
[draft-ietf-hpke-hpke-02]: https://datatracker.ietf.org/doc/html/draft-ietf-hpke-hpke-02
|
||||
[draft-ietf-hpke-pq-03]: https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-03
|
||||
[draft-irtf-cfrg-hybrid-kems-07]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hybrid-kems-07
|
||||
[draft-irtf-cfrg-concrete-hybrid-kems-02]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-concrete-hybrid-kems-02
|
||||
[RFC 7748]: https://rfc-editor.org/rfc/rfc7748.html
|
||||
[RFC 7748, Section 4.1]: https://rfc-editor.org/rfc/rfc7748.html#section-4.1
|
||||
[RFC 7748, Section 6.1]: https://rfc-editor.org/rfc/rfc7748.html#section-6.1
|
||||
[RFC 8017]: https://datatracker.ietf.org/doc/html/rfc8017
|
||||
[FIPS 202]: https://doi.org/10.6028/NIST.FIPS.202
|
||||
[FIPS 203]: https://doi.org/10.6028/NIST.FIPS.203
|
||||
[SP800-186]: https://doi.org/10.6028/NIST.SP.800-186
|
||||
[SEC 1, Version 2.0]: https://www.secg.org/sec1-v2.pdf
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package hpke implements Hybrid Public Key Encryption (HPKE) as defined in
|
||||
// [RFC 9180].
|
||||
//
|
||||
// [RFC 9180]: https://www.rfc-editor.org/rfc/rfc9180.html
|
||||
package hpke
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
|
||||
"filippo.io/hpke/internal/byteorder"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
suiteID []byte
|
||||
|
||||
export func(string, uint16) ([]byte, error)
|
||||
|
||||
aead cipher.AEAD
|
||||
baseNonce []byte
|
||||
// seqNum starts at zero and is incremented for each Seal/Open call.
|
||||
// 64 bits are enough not to overflow for 500 years at 1ns per operation.
|
||||
seqNum uint64
|
||||
}
|
||||
|
||||
// Sender is a sending HPKE context. It is instantiated with a specific KEM
|
||||
// encapsulation key (i.e. the public key), and it is stateful, incrementing the
|
||||
// nonce counter for each [Sender.Seal] call.
|
||||
type Sender struct {
|
||||
*context
|
||||
}
|
||||
|
||||
// Recipient is a receiving HPKE context. It is instantiated with a specific KEM
|
||||
// decapsulation key (i.e. the secret key), and it is stateful, incrementing the
|
||||
// nonce counter for each successful [Recipient.Open] call.
|
||||
type Recipient struct {
|
||||
*context
|
||||
}
|
||||
|
||||
func newContext(sharedSecret []byte, kemID uint16, kdf KDF, aead AEAD, info []byte) (*context, error) {
|
||||
sid := suiteID(kemID, kdf.ID(), aead.ID())
|
||||
|
||||
if kdf.oneStage() {
|
||||
secrets := make([]byte, 0, 2+2+len(sharedSecret))
|
||||
secrets = byteorder.BEAppendUint16(secrets, 0) // empty psk
|
||||
secrets = byteorder.BEAppendUint16(secrets, uint16(len(sharedSecret)))
|
||||
secrets = append(secrets, sharedSecret...)
|
||||
|
||||
ksContext := make([]byte, 0, 1+2+2+len(info))
|
||||
ksContext = append(ksContext, 0) // mode 0
|
||||
ksContext = byteorder.BEAppendUint16(ksContext, 0) // empty psk_id
|
||||
ksContext = byteorder.BEAppendUint16(ksContext, uint16(len(info)))
|
||||
ksContext = append(ksContext, info...)
|
||||
|
||||
secret, err := kdf.labeledDerive(sid, secrets, "secret", ksContext,
|
||||
uint16(aead.keySize()+aead.nonceSize()+kdf.size()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := secret[:aead.keySize()]
|
||||
baseNonce := secret[aead.keySize() : aead.keySize()+aead.nonceSize()]
|
||||
expSecret := secret[aead.keySize()+aead.nonceSize():]
|
||||
|
||||
a, err := aead.aead(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
export := func(exporterContext string, length uint16) ([]byte, error) {
|
||||
return kdf.labeledDerive(sid, expSecret, "sec", []byte(exporterContext), length)
|
||||
}
|
||||
|
||||
return &context{
|
||||
aead: a,
|
||||
suiteID: sid,
|
||||
export: export,
|
||||
baseNonce: baseNonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
pskIDHash, err := kdf.labeledExtract(sid, nil, "psk_id_hash", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoHash, err := kdf.labeledExtract(sid, nil, "info_hash", info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ksContext := append([]byte{0}, pskIDHash...)
|
||||
ksContext = append(ksContext, infoHash...)
|
||||
|
||||
secret, err := kdf.labeledExtract(sid, sharedSecret, "secret", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := kdf.labeledExpand(sid, secret, "key", ksContext, uint16(aead.keySize()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a, err := aead.aead(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseNonce, err := kdf.labeledExpand(sid, secret, "base_nonce", ksContext, uint16(aead.nonceSize()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expSecret, err := kdf.labeledExpand(sid, secret, "exp", ksContext, uint16(kdf.size()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
export := func(exporterContext string, length uint16) ([]byte, error) {
|
||||
return kdf.labeledExpand(sid, expSecret, "sec", []byte(exporterContext), length)
|
||||
}
|
||||
|
||||
return &context{
|
||||
aead: a,
|
||||
suiteID: sid,
|
||||
export: export,
|
||||
baseNonce: baseNonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSender returns a sending HPKE context for the provided KEM encapsulation
|
||||
// key (i.e. the public key), and using the ciphersuite defined by the
|
||||
// combination of KEM, KDF, and AEAD.
|
||||
//
|
||||
// The info parameter is additional public information that must match between
|
||||
// sender and recipient.
|
||||
//
|
||||
// The returned enc ciphertext can be used to instantiate a matching receiving
|
||||
// HPKE context with the corresponding KEM decapsulation key.
|
||||
func NewSender(pk PublicKey, kdf KDF, aead AEAD, info []byte) (enc []byte, s *Sender, err error) {
|
||||
sharedSecret, encapsulatedKey, err := pk.encap()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
context, err := newContext(sharedSecret, pk.KEM().ID(), kdf, aead, info)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return encapsulatedKey, &Sender{context}, nil
|
||||
}
|
||||
|
||||
// NewRecipient returns a receiving HPKE context for the provided KEM
|
||||
// decapsulation key (i.e. the secret key), and using the ciphersuite defined by
|
||||
// the combination of KEM, KDF, and AEAD.
|
||||
//
|
||||
// The enc parameter must have been produced by a matching sending HPKE context
|
||||
// with the corresponding KEM encapsulation key. The info parameter is
|
||||
// additional public information that must match between sender and recipient.
|
||||
func NewRecipient(enc []byte, k PrivateKey, kdf KDF, aead AEAD, info []byte) (*Recipient, error) {
|
||||
sharedSecret, err := k.decap(enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
context, err := newContext(sharedSecret, k.KEM().ID(), kdf, aead, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Recipient{context}, nil
|
||||
}
|
||||
|
||||
// Seal encrypts the provided plaintext, optionally binding to the additional
|
||||
// public data aad.
|
||||
//
|
||||
// Seal uses incrementing counters for each call, and Open on the receiving side
|
||||
// must be called in the same order as Seal.
|
||||
func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) {
|
||||
if s.aead == nil {
|
||||
return nil, errors.New("export-only instantiation")
|
||||
}
|
||||
ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad)
|
||||
s.seqNum++
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// Seal instantiates a single-use HPKE sending HPKE context like [NewSender],
|
||||
// and then encrypts the provided plaintext like [Sender.Seal] (with no aad).
|
||||
// Seal returns the concatenation of the encapsulated key and the ciphertext.
|
||||
func Seal(pk PublicKey, kdf KDF, aead AEAD, info, plaintext []byte) ([]byte, error) {
|
||||
enc, s, err := NewSender(pk, kdf, aead, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct, err := s.Seal(nil, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(enc, ct...), nil
|
||||
}
|
||||
|
||||
// Export produces a secret value derived from the shared key between sender and
|
||||
// recipient. length must be at most 65,535.
|
||||
func (s *Sender) Export(exporterContext string, length int) ([]byte, error) {
|
||||
if length < 0 || length > 0xFFFF {
|
||||
return nil, errors.New("invalid length")
|
||||
}
|
||||
return s.export(exporterContext, uint16(length))
|
||||
}
|
||||
|
||||
// Open decrypts the provided ciphertext, optionally binding to the additional
|
||||
// public data aad, or returns an error if decryption fails.
|
||||
//
|
||||
// Open uses incrementing counters for each successful call, and must be called
|
||||
// in the same order as Seal on the sending side.
|
||||
func (r *Recipient) Open(aad, ciphertext []byte) ([]byte, error) {
|
||||
if r.aead == nil {
|
||||
return nil, errors.New("export-only instantiation")
|
||||
}
|
||||
plaintext, err := r.aead.Open(nil, r.nextNonce(), ciphertext, aad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.seqNum++
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// Open instantiates a single-use HPKE receiving HPKE context like [NewRecipient],
|
||||
// and then decrypts the provided ciphertext like [Recipient.Open] (with no aad).
|
||||
// ciphertext must be the concatenation of the encapsulated key and the actual ciphertext.
|
||||
func Open(k PrivateKey, kdf KDF, aead AEAD, info, ciphertext []byte) ([]byte, error) {
|
||||
encSize := k.KEM().encSize()
|
||||
if len(ciphertext) < encSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
enc, ciphertext := ciphertext[:encSize], ciphertext[encSize:]
|
||||
r, err := NewRecipient(enc, k, kdf, aead, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Open(nil, ciphertext)
|
||||
}
|
||||
|
||||
// Export produces a secret value derived from the shared key between sender and
|
||||
// recipient. length must be at most 65,535.
|
||||
func (r *Recipient) Export(exporterContext string, length int) ([]byte, error) {
|
||||
if length < 0 || length > 0xFFFF {
|
||||
return nil, errors.New("invalid length")
|
||||
}
|
||||
return r.export(exporterContext, uint16(length))
|
||||
}
|
||||
|
||||
func (ctx *context) nextNonce() []byte {
|
||||
nonce := make([]byte, ctx.aead.NonceSize())
|
||||
byteorder.BEPutUint64(nonce[len(nonce)-8:], ctx.seqNum)
|
||||
for i := range ctx.baseNonce {
|
||||
nonce[i] ^= ctx.baseNonce[i]
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
func suiteID(kemID, kdfID, aeadID uint16) []byte {
|
||||
suiteID := make([]byte, 0, 4+2+2+2)
|
||||
suiteID = append(suiteID, []byte("HPKE")...)
|
||||
suiteID = byteorder.BEAppendUint16(suiteID, kemID)
|
||||
suiteID = byteorder.BEAppendUint16(suiteID, kdfID)
|
||||
suiteID = byteorder.BEAppendUint16(suiteID, aeadID)
|
||||
return suiteID
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package byteorder provides functions for decoding and encoding
|
||||
// little and big endian integer types from/to byte slices.
|
||||
package byteorder
|
||||
|
||||
func LEUint16(b []byte) uint16 {
|
||||
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint16(b[0]) | uint16(b[1])<<8
|
||||
}
|
||||
|
||||
func LEPutUint16(b []byte, v uint16) {
|
||||
_ = b[1] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v)
|
||||
b[1] = byte(v >> 8)
|
||||
}
|
||||
|
||||
func LEAppendUint16(b []byte, v uint16) []byte {
|
||||
return append(b,
|
||||
byte(v),
|
||||
byte(v>>8),
|
||||
)
|
||||
}
|
||||
|
||||
func LEUint32(b []byte) uint32 {
|
||||
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
}
|
||||
|
||||
func LEPutUint32(b []byte, v uint32) {
|
||||
_ = b[3] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v >> 16)
|
||||
b[3] = byte(v >> 24)
|
||||
}
|
||||
|
||||
func LEAppendUint32(b []byte, v uint32) []byte {
|
||||
return append(b,
|
||||
byte(v),
|
||||
byte(v>>8),
|
||||
byte(v>>16),
|
||||
byte(v>>24),
|
||||
)
|
||||
}
|
||||
|
||||
func LEUint64(b []byte) uint64 {
|
||||
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
|
||||
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
|
||||
}
|
||||
|
||||
func LEPutUint64(b []byte, v uint64) {
|
||||
_ = b[7] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v)
|
||||
b[1] = byte(v >> 8)
|
||||
b[2] = byte(v >> 16)
|
||||
b[3] = byte(v >> 24)
|
||||
b[4] = byte(v >> 32)
|
||||
b[5] = byte(v >> 40)
|
||||
b[6] = byte(v >> 48)
|
||||
b[7] = byte(v >> 56)
|
||||
}
|
||||
|
||||
func LEAppendUint64(b []byte, v uint64) []byte {
|
||||
return append(b,
|
||||
byte(v),
|
||||
byte(v>>8),
|
||||
byte(v>>16),
|
||||
byte(v>>24),
|
||||
byte(v>>32),
|
||||
byte(v>>40),
|
||||
byte(v>>48),
|
||||
byte(v>>56),
|
||||
)
|
||||
}
|
||||
|
||||
func BEUint16(b []byte) uint16 {
|
||||
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint16(b[1]) | uint16(b[0])<<8
|
||||
}
|
||||
|
||||
func BEPutUint16(b []byte, v uint16) {
|
||||
_ = b[1] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v >> 8)
|
||||
b[1] = byte(v)
|
||||
}
|
||||
|
||||
func BEAppendUint16(b []byte, v uint16) []byte {
|
||||
return append(b,
|
||||
byte(v>>8),
|
||||
byte(v),
|
||||
)
|
||||
}
|
||||
|
||||
func BEUint32(b []byte) uint32 {
|
||||
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
|
||||
}
|
||||
|
||||
func BEPutUint32(b []byte, v uint32) {
|
||||
_ = b[3] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v >> 24)
|
||||
b[1] = byte(v >> 16)
|
||||
b[2] = byte(v >> 8)
|
||||
b[3] = byte(v)
|
||||
}
|
||||
|
||||
func BEAppendUint32(b []byte, v uint32) []byte {
|
||||
return append(b,
|
||||
byte(v>>24),
|
||||
byte(v>>16),
|
||||
byte(v>>8),
|
||||
byte(v),
|
||||
)
|
||||
}
|
||||
|
||||
func BEUint64(b []byte) uint64 {
|
||||
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
|
||||
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
|
||||
}
|
||||
|
||||
func BEPutUint64(b []byte, v uint64) {
|
||||
_ = b[7] // early bounds check to guarantee safety of writes below
|
||||
b[0] = byte(v >> 56)
|
||||
b[1] = byte(v >> 48)
|
||||
b[2] = byte(v >> 40)
|
||||
b[3] = byte(v >> 32)
|
||||
b[4] = byte(v >> 24)
|
||||
b[5] = byte(v >> 16)
|
||||
b[6] = byte(v >> 8)
|
||||
b[7] = byte(v)
|
||||
}
|
||||
|
||||
func BEAppendUint64(b []byte, v uint64) []byte {
|
||||
return append(b,
|
||||
byte(v>>56),
|
||||
byte(v>>48),
|
||||
byte(v>>40),
|
||||
byte(v>>32),
|
||||
byte(v>>24),
|
||||
byte(v>>16),
|
||||
byte(v>>8),
|
||||
byte(v),
|
||||
)
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package hpke
|
||||
|
||||
import (
|
||||
"crypto/hkdf"
|
||||
"crypto/sha256"
|
||||
"crypto/sha3"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"filippo.io/hpke/internal/byteorder"
|
||||
)
|
||||
|
||||
// The KDF is one of the three components of an HPKE ciphersuite, implementing
|
||||
// key derivation.
|
||||
type KDF interface {
|
||||
ID() uint16
|
||||
oneStage() bool
|
||||
size() int // Nh
|
||||
labeledDerive(suiteID, inputKey []byte, label string, context []byte, length uint16) ([]byte, error)
|
||||
labeledExtract(suiteID, salt []byte, label string, inputKey []byte) ([]byte, error)
|
||||
labeledExpand(suiteID, randomKey []byte, label string, info []byte, length uint16) ([]byte, error)
|
||||
}
|
||||
|
||||
// NewKDF returns the KDF implementation for the given KDF ID.
|
||||
//
|
||||
// Applications are encouraged to use specific implementations like [HKDFSHA256]
|
||||
// instead, unless runtime agility is required.
|
||||
func NewKDF(id uint16) (KDF, error) {
|
||||
switch id {
|
||||
case 0x0001: // HKDF-SHA256
|
||||
return HKDFSHA256(), nil
|
||||
case 0x0002: // HKDF-SHA384
|
||||
return HKDFSHA384(), nil
|
||||
case 0x0003: // HKDF-SHA512
|
||||
return HKDFSHA512(), nil
|
||||
case 0x0010: // SHAKE128
|
||||
return SHAKE128(), nil
|
||||
case 0x0011: // SHAKE256
|
||||
return SHAKE256(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported KDF %04x", id)
|
||||
}
|
||||
}
|
||||
|
||||
// HKDFSHA256 returns an HKDF-SHA256 KDF implementation.
|
||||
func HKDFSHA256() KDF { return hkdfSHA256 }
|
||||
|
||||
// HKDFSHA384 returns an HKDF-SHA384 KDF implementation.
|
||||
func HKDFSHA384() KDF { return hkdfSHA384 }
|
||||
|
||||
// HKDFSHA512 returns an HKDF-SHA512 KDF implementation.
|
||||
func HKDFSHA512() KDF { return hkdfSHA512 }
|
||||
|
||||
type hkdfKDF struct {
|
||||
hash func() hash.Hash
|
||||
id uint16
|
||||
nH int
|
||||
}
|
||||
|
||||
var hkdfSHA256 = &hkdfKDF{hash: sha256.New, id: 0x0001, nH: sha256.Size}
|
||||
var hkdfSHA384 = &hkdfKDF{hash: sha512.New384, id: 0x0002, nH: sha512.Size384}
|
||||
var hkdfSHA512 = &hkdfKDF{hash: sha512.New, id: 0x0003, nH: sha512.Size}
|
||||
|
||||
func (kdf *hkdfKDF) ID() uint16 {
|
||||
return kdf.id
|
||||
}
|
||||
|
||||
func (kdf *hkdfKDF) size() int {
|
||||
return kdf.nH
|
||||
}
|
||||
|
||||
func (kdf *hkdfKDF) oneStage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (kdf *hkdfKDF) labeledDerive(_, _ []byte, _ string, _ []byte, _ uint16) ([]byte, error) {
|
||||
return nil, errors.New("hpke: internal error: labeledDerive called on two-stage KDF")
|
||||
}
|
||||
|
||||
func (kdf *hkdfKDF) labeledExtract(suiteID []byte, salt []byte, label string, inputKey []byte) ([]byte, error) {
|
||||
labeledIKM := make([]byte, 0, 7+len(suiteID)+len(label)+len(inputKey))
|
||||
labeledIKM = append(labeledIKM, []byte("HPKE-v1")...)
|
||||
labeledIKM = append(labeledIKM, suiteID...)
|
||||
labeledIKM = append(labeledIKM, label...)
|
||||
labeledIKM = append(labeledIKM, inputKey...)
|
||||
return hkdf.Extract(kdf.hash, labeledIKM, salt)
|
||||
}
|
||||
|
||||
func (kdf *hkdfKDF) labeledExpand(suiteID []byte, randomKey []byte, label string, info []byte, length uint16) ([]byte, error) {
|
||||
labeledInfo := make([]byte, 0, 2+7+len(suiteID)+len(label)+len(info))
|
||||
labeledInfo = byteorder.BEAppendUint16(labeledInfo, length)
|
||||
labeledInfo = append(labeledInfo, []byte("HPKE-v1")...)
|
||||
labeledInfo = append(labeledInfo, suiteID...)
|
||||
labeledInfo = append(labeledInfo, label...)
|
||||
labeledInfo = append(labeledInfo, info...)
|
||||
return hkdf.Expand(kdf.hash, randomKey, string(labeledInfo), int(length))
|
||||
}
|
||||
|
||||
// SHAKE128 returns a SHAKE128 KDF implementation.
|
||||
func SHAKE128() KDF {
|
||||
return shake128KDF
|
||||
}
|
||||
|
||||
// SHAKE256 returns a SHAKE256 KDF implementation.
|
||||
func SHAKE256() KDF {
|
||||
return shake256KDF
|
||||
}
|
||||
|
||||
type shakeKDF struct {
|
||||
hash func() *sha3.SHAKE
|
||||
id uint16
|
||||
nH int
|
||||
}
|
||||
|
||||
var shake128KDF = &shakeKDF{hash: sha3.NewSHAKE128, id: 0x0010, nH: 32}
|
||||
var shake256KDF = &shakeKDF{hash: sha3.NewSHAKE256, id: 0x0011, nH: 64}
|
||||
|
||||
func (kdf *shakeKDF) ID() uint16 {
|
||||
return kdf.id
|
||||
}
|
||||
|
||||
func (kdf *shakeKDF) size() int {
|
||||
return kdf.nH
|
||||
}
|
||||
|
||||
func (kdf *shakeKDF) oneStage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (kdf *shakeKDF) labeledDerive(suiteID, inputKey []byte, label string, context []byte, length uint16) ([]byte, error) {
|
||||
H := kdf.hash()
|
||||
H.Write(inputKey)
|
||||
H.Write([]byte("HPKE-v1"))
|
||||
H.Write(suiteID)
|
||||
H.Write([]byte{byte(len(label) >> 8), byte(len(label))})
|
||||
H.Write([]byte(label))
|
||||
H.Write([]byte{byte(length >> 8), byte(length)})
|
||||
H.Write(context)
|
||||
out := make([]byte, length)
|
||||
H.Read(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (kdf *shakeKDF) labeledExtract(_, _ []byte, _ string, _ []byte) ([]byte, error) {
|
||||
return nil, errors.New("hpke: internal error: labeledExtract called on one-stage KDF")
|
||||
}
|
||||
|
||||
func (kdf *shakeKDF) labeledExpand(_, _ []byte, _ string, _ []byte, _ uint16) ([]byte, error) {
|
||||
return nil, errors.New("hpke: internal error: labeledExpand called on one-stage KDF")
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package hpke
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
|
||||
"filippo.io/hpke/crypto/ecdh"
|
||||
"filippo.io/hpke/internal/byteorder"
|
||||
)
|
||||
|
||||
// A KEM is a Key Encapsulation Mechanism, one of the three components of an
|
||||
// HPKE ciphersuite.
|
||||
type KEM interface {
|
||||
// ID returns the HPKE KEM identifier.
|
||||
ID() uint16
|
||||
|
||||
// GenerateKey generates a new key pair.
|
||||
GenerateKey() (PrivateKey, error)
|
||||
|
||||
// NewPublicKey deserializes a public key from bytes.
|
||||
//
|
||||
// It implements DeserializePublicKey, as defined in RFC 9180.
|
||||
NewPublicKey([]byte) (PublicKey, error)
|
||||
|
||||
// NewPrivateKey deserializes a private key from bytes.
|
||||
//
|
||||
// It implements DeserializePrivateKey, as defined in RFC 9180.
|
||||
NewPrivateKey([]byte) (PrivateKey, error)
|
||||
|
||||
// DeriveKeyPair derives a key pair from the given input keying material.
|
||||
//
|
||||
// It implements DeriveKeyPair, as defined in RFC 9180.
|
||||
DeriveKeyPair(ikm []byte) (PrivateKey, error)
|
||||
|
||||
encSize() int
|
||||
}
|
||||
|
||||
// NewKEM returns the KEM implementation for the given KEM ID.
|
||||
//
|
||||
// Applications are encouraged to use specific implementations like [DHKEM] or
|
||||
// [MLKEM768X25519] instead, unless runtime agility is required.
|
||||
func NewKEM(id uint16) (KEM, error) {
|
||||
switch id {
|
||||
case 0x0010: // DHKEM(P-256, HKDF-SHA256)
|
||||
return DHKEM(ecdh.P256()), nil
|
||||
case 0x0011: // DHKEM(P-384, HKDF-SHA384)
|
||||
return DHKEM(ecdh.P384()), nil
|
||||
case 0x0012: // DHKEM(P-521, HKDF-SHA512)
|
||||
return DHKEM(ecdh.P521()), nil
|
||||
case 0x0020: // DHKEM(X25519, HKDF-SHA256)
|
||||
return DHKEM(ecdh.X25519()), nil
|
||||
case 0x0041: // ML-KEM-768
|
||||
return MLKEM768(), nil
|
||||
case 0x0042: // ML-KEM-1024
|
||||
return MLKEM1024(), nil
|
||||
case 0x647a: // MLKEM768-X25519
|
||||
return MLKEM768X25519(), nil
|
||||
case 0x0050: // MLKEM768-P256
|
||||
return MLKEM768P256(), nil
|
||||
case 0x0051: // MLKEM1024-P384
|
||||
return MLKEM1024P384(), nil
|
||||
default:
|
||||
return nil, errors.New("unsupported KEM")
|
||||
}
|
||||
}
|
||||
|
||||
// A PublicKey is an instantiation of a KEM (one of the three components of an
|
||||
// HPKE ciphersuite) with an encapsulation key (i.e. the public key).
|
||||
//
|
||||
// A PublicKey is usually obtained from a method of the corresponding [KEM] or
|
||||
// [PrivateKey], such as [KEM.NewPublicKey] or [PrivateKey.PublicKey].
|
||||
type PublicKey interface {
|
||||
// KEM returns the instantiated KEM.
|
||||
KEM() KEM
|
||||
|
||||
// Bytes returns the public key as the output of SerializePublicKey.
|
||||
Bytes() []byte
|
||||
|
||||
encap() (sharedSecret, enc []byte, err error)
|
||||
}
|
||||
|
||||
// A PrivateKey is an instantiation of a KEM (one of the three components of
|
||||
// an HPKE ciphersuite) with a decapsulation key (i.e. the secret key).
|
||||
//
|
||||
// A PrivateKey is usually obtained from a method of the corresponding [KEM],
|
||||
// such as [KEM.GenerateKey] or [KEM.NewPrivateKey].
|
||||
type PrivateKey interface {
|
||||
// KEM returns the instantiated KEM.
|
||||
KEM() KEM
|
||||
|
||||
// Bytes returns the private key as the output of SerializePrivateKey, as
|
||||
// defined in RFC 9180.
|
||||
//
|
||||
// Note that for X25519 this might not match the input to NewPrivateKey.
|
||||
// This is a requirement of RFC 9180, Section 7.1.2.
|
||||
Bytes() ([]byte, error)
|
||||
|
||||
// PublicKey returns the corresponding PublicKey.
|
||||
PublicKey() PublicKey
|
||||
|
||||
decap(enc []byte) (sharedSecret []byte, err error)
|
||||
}
|
||||
|
||||
type dhKEM struct {
|
||||
kdf KDF
|
||||
id uint16
|
||||
curve ecdh.Curve
|
||||
Nsecret uint16
|
||||
Nsk uint16
|
||||
Nenc int
|
||||
}
|
||||
|
||||
func (kem *dhKEM) extractAndExpand(dhKey, kemContext []byte) ([]byte, error) {
|
||||
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
|
||||
eaePRK, err := kem.kdf.labeledExtract(suiteID, nil, "eae_prk", dhKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kem.kdf.labeledExpand(suiteID, eaePRK, "shared_secret", kemContext, kem.Nsecret)
|
||||
}
|
||||
|
||||
func (kem *dhKEM) ID() uint16 {
|
||||
return kem.id
|
||||
}
|
||||
|
||||
func (kem *dhKEM) encSize() int {
|
||||
return kem.Nenc
|
||||
}
|
||||
|
||||
var dhKEMP256 = &dhKEM{HKDFSHA256(), 0x0010, ecdh.P256(), 32, 32, 65}
|
||||
var dhKEMP384 = &dhKEM{HKDFSHA384(), 0x0011, ecdh.P384(), 48, 48, 97}
|
||||
var dhKEMP521 = &dhKEM{HKDFSHA512(), 0x0012, ecdh.P521(), 64, 66, 133}
|
||||
var dhKEMX25519 = &dhKEM{HKDFSHA256(), 0x0020, ecdh.X25519(), 32, 32, 32}
|
||||
|
||||
// DHKEM returns a KEM implementing one of
|
||||
//
|
||||
// - DHKEM(P-256, HKDF-SHA256)
|
||||
// - DHKEM(P-384, HKDF-SHA384)
|
||||
// - DHKEM(P-521, HKDF-SHA512)
|
||||
// - DHKEM(X25519, HKDF-SHA256)
|
||||
//
|
||||
// depending on curve.
|
||||
func DHKEM(curve ecdh.Curve) KEM {
|
||||
switch curve {
|
||||
case ecdh.P256():
|
||||
return dhKEMP256
|
||||
case ecdh.P384():
|
||||
return dhKEMP384
|
||||
case ecdh.P521():
|
||||
return dhKEMP521
|
||||
case ecdh.X25519():
|
||||
return dhKEMX25519
|
||||
default:
|
||||
// The set of ecdh.Curve implementations is closed, because the
|
||||
// interface has unexported methods. Therefore, this default case is
|
||||
// only hit if a new curve is added that DHKEM doesn't support.
|
||||
return unsupportedCurveKEM{}
|
||||
}
|
||||
}
|
||||
|
||||
type unsupportedCurveKEM struct{}
|
||||
|
||||
func (unsupportedCurveKEM) ID() uint16 {
|
||||
return 0
|
||||
}
|
||||
func (unsupportedCurveKEM) GenerateKey() (PrivateKey, error) {
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
func (unsupportedCurveKEM) NewPublicKey([]byte) (PublicKey, error) {
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
func (unsupportedCurveKEM) NewPrivateKey([]byte) (PrivateKey, error) {
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
func (unsupportedCurveKEM) DeriveKeyPair([]byte) (PrivateKey, error) {
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
func (unsupportedCurveKEM) encSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type dhKEMPublicKey struct {
|
||||
kem *dhKEM
|
||||
pub *ecdh.PublicKey
|
||||
}
|
||||
|
||||
// NewDHKEMPublicKey returns a PublicKey implementing
|
||||
//
|
||||
// - DHKEM(P-256, HKDF-SHA256)
|
||||
// - DHKEM(P-384, HKDF-SHA384)
|
||||
// - DHKEM(P-521, HKDF-SHA512)
|
||||
// - DHKEM(X25519, HKDF-SHA256)
|
||||
//
|
||||
// depending on the underlying curve of pub ([ecdh.X25519], [ecdh.P256],
|
||||
// [ecdh.P384], or [ecdh.P521]).
|
||||
//
|
||||
// This function is meant for applications that already have an instantiated
|
||||
// crypto/ecdh public key. Otherwise, applications should use the
|
||||
// [KEM.NewPublicKey] method of [DHKEM].
|
||||
func NewDHKEMPublicKey(pub *ecdh.PublicKey) (PublicKey, error) {
|
||||
kem, ok := DHKEM(pub.Curve()).(*dhKEM)
|
||||
if !ok {
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
return &dhKEMPublicKey{
|
||||
kem: kem,
|
||||
pub: pub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (kem *dhKEM) NewPublicKey(data []byte) (PublicKey, error) {
|
||||
pub, err := kem.curve.NewPublicKey(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDHKEMPublicKey(pub)
|
||||
}
|
||||
|
||||
func (pk *dhKEMPublicKey) KEM() KEM {
|
||||
return pk.kem
|
||||
}
|
||||
|
||||
func (pk *dhKEMPublicKey) Bytes() []byte {
|
||||
return pk.pub.Bytes()
|
||||
}
|
||||
|
||||
// testingOnlyGenerateKey is only used during testing, to provide
|
||||
// a fixed test key to use when checking the RFC 9180 vectors.
|
||||
var testingOnlyGenerateKey func() *ecdh.PrivateKey
|
||||
|
||||
func (pk *dhKEMPublicKey) encap() (sharedSecret []byte, encapPub []byte, err error) {
|
||||
privEph, err := pk.pub.Curve().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if testingOnlyGenerateKey != nil {
|
||||
privEph = testingOnlyGenerateKey()
|
||||
}
|
||||
dhVal, err := privEph.ECDH(pk.pub)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
encPubEph := privEph.PublicKey().Bytes()
|
||||
|
||||
encPubRecip := pk.pub.Bytes()
|
||||
kemContext := append(encPubEph, encPubRecip...)
|
||||
sharedSecret, err = pk.kem.extractAndExpand(dhVal, kemContext)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sharedSecret, encPubEph, nil
|
||||
}
|
||||
|
||||
type dhKEMPrivateKey struct {
|
||||
kem *dhKEM
|
||||
priv ecdh.KeyExchanger
|
||||
}
|
||||
|
||||
// NewDHKEMPrivateKey returns a PrivateKey implementing
|
||||
//
|
||||
// - DHKEM(P-256, HKDF-SHA256)
|
||||
// - DHKEM(P-384, HKDF-SHA384)
|
||||
// - DHKEM(P-521, HKDF-SHA512)
|
||||
// - DHKEM(X25519, HKDF-SHA256)
|
||||
//
|
||||
// depending on the underlying curve of priv ([ecdh.X25519], [ecdh.P256],
|
||||
// [ecdh.P384], or [ecdh.P521]).
|
||||
//
|
||||
// This function is meant for applications that already have an instantiated
|
||||
// crypto/ecdh private key, or another implementation of a [ecdh.KeyExchanger]
|
||||
// (e.g. a hardware key). Otherwise, applications should use the
|
||||
// [KEM.NewPrivateKey] method of [DHKEM].
|
||||
func NewDHKEMPrivateKey(priv ecdh.KeyExchanger) (PrivateKey, error) {
|
||||
kem, ok := DHKEM(priv.Curve()).(*dhKEM)
|
||||
if !ok {
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
return &dhKEMPrivateKey{
|
||||
kem: kem,
|
||||
priv: priv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (kem *dhKEM) GenerateKey() (PrivateKey, error) {
|
||||
priv, err := kem.curve.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDHKEMPrivateKey(priv)
|
||||
}
|
||||
|
||||
func (kem *dhKEM) NewPrivateKey(ikm []byte) (PrivateKey, error) {
|
||||
priv, err := kem.curve.NewPrivateKey(ikm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDHKEMPrivateKey(priv)
|
||||
}
|
||||
|
||||
func (kem *dhKEM) DeriveKeyPair(ikm []byte) (PrivateKey, error) {
|
||||
// DeriveKeyPair from RFC 9180 Section 7.1.3.
|
||||
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
|
||||
prk, err := kem.kdf.labeledExtract(suiteID, nil, "dkp_prk", ikm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kem == dhKEMX25519 {
|
||||
s, err := kem.kdf.labeledExpand(suiteID, prk, "sk", nil, kem.Nsk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kem.NewPrivateKey(s)
|
||||
}
|
||||
var counter uint8
|
||||
for counter < 4 {
|
||||
s, err := kem.kdf.labeledExpand(suiteID, prk, "candidate", []byte{counter}, kem.Nsk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kem == dhKEMP521 {
|
||||
s[0] &= 0x01
|
||||
}
|
||||
r, err := kem.NewPrivateKey(s)
|
||||
if err != nil {
|
||||
counter++
|
||||
continue
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
panic("chance of four rejections is < 2^-128")
|
||||
}
|
||||
|
||||
func (k *dhKEMPrivateKey) KEM() KEM {
|
||||
return k.kem
|
||||
}
|
||||
|
||||
func (k *dhKEMPrivateKey) Bytes() ([]byte, error) {
|
||||
// Bizarrely, RFC 9180, Section 7.1.2 says SerializePrivateKey MUST clamp
|
||||
// the output, which I thought we all agreed to instead do as part of the DH
|
||||
// function, letting private keys be random bytes.
|
||||
//
|
||||
// At the same time, it says DeserializePrivateKey MUST also clamp, implying
|
||||
// that the input doesn't have to be clamped, so Bytes by spec doesn't
|
||||
// necessarily match the NewPrivateKey input.
|
||||
//
|
||||
// I'm sure this will not lead to any unexpected behavior or interop issue.
|
||||
priv, ok := k.priv.(*ecdh.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("ecdh: private key does not support Bytes")
|
||||
}
|
||||
if k.kem == dhKEMX25519 {
|
||||
b := priv.Bytes()
|
||||
b[0] &= 248
|
||||
b[31] &= 127
|
||||
b[31] |= 64
|
||||
return b, nil
|
||||
}
|
||||
return priv.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *dhKEMPrivateKey) PublicKey() PublicKey {
|
||||
return &dhKEMPublicKey{
|
||||
kem: k.kem,
|
||||
pub: k.priv.PublicKey(),
|
||||
}
|
||||
}
|
||||
|
||||
func (k *dhKEMPrivateKey) decap(encPubEph []byte) ([]byte, error) {
|
||||
pubEph, err := k.priv.Curve().NewPublicKey(encPubEph)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dhVal, err := k.priv.ECDH(pubEph)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kemContext := append(encPubEph, k.priv.PublicKey().Bytes()...)
|
||||
return k.kem.extractAndExpand(dhVal, kemContext)
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package hpke
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/mlkem"
|
||||
"crypto/rand"
|
||||
"crypto/sha3"
|
||||
"errors"
|
||||
|
||||
"filippo.io/hpke/crypto"
|
||||
"filippo.io/hpke/crypto/ecdh"
|
||||
"filippo.io/hpke/internal/byteorder"
|
||||
)
|
||||
|
||||
var mlkem768X25519 = &hybridKEM{
|
||||
id: 0x647a,
|
||||
label: /**/ `\./` +
|
||||
/* */ `/^\`,
|
||||
curve: ecdh.X25519(),
|
||||
|
||||
curveSeedSize: 32,
|
||||
curvePointSize: 32,
|
||||
pqEncapsKeySize: mlkem.EncapsulationKeySize768,
|
||||
pqCiphertextSize: mlkem.CiphertextSize768,
|
||||
|
||||
pqNewPublicKey: func(data []byte) (crypto.Encapsulator, error) {
|
||||
return mlkem.NewEncapsulationKey768(data)
|
||||
},
|
||||
pqNewPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.NewDecapsulationKey768(data))
|
||||
},
|
||||
}
|
||||
|
||||
// MLKEM768X25519 returns a KEM implementing MLKEM768-X25519 (a.k.a. X-Wing)
|
||||
// from draft-ietf-hpke-pq.
|
||||
func MLKEM768X25519() KEM {
|
||||
return mlkem768X25519
|
||||
}
|
||||
|
||||
var mlkem768P256 = &hybridKEM{
|
||||
id: 0x0050,
|
||||
label: "MLKEM768-P256",
|
||||
curve: ecdh.P256(),
|
||||
|
||||
curveSeedSize: 32,
|
||||
curvePointSize: 65,
|
||||
pqEncapsKeySize: mlkem.EncapsulationKeySize768,
|
||||
pqCiphertextSize: mlkem.CiphertextSize768,
|
||||
|
||||
pqNewPublicKey: func(data []byte) (crypto.Encapsulator, error) {
|
||||
return mlkem.NewEncapsulationKey768(data)
|
||||
},
|
||||
pqNewPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.NewDecapsulationKey768(data))
|
||||
},
|
||||
}
|
||||
|
||||
// MLKEM768P256 returns a KEM implementing MLKEM768-P256 from draft-ietf-hpke-pq.
|
||||
func MLKEM768P256() KEM {
|
||||
return mlkem768P256
|
||||
}
|
||||
|
||||
var mlkem1024P384 = &hybridKEM{
|
||||
id: 0x0051,
|
||||
label: "MLKEM1024-P384",
|
||||
curve: ecdh.P384(),
|
||||
|
||||
curveSeedSize: 48,
|
||||
curvePointSize: 97,
|
||||
pqEncapsKeySize: mlkem.EncapsulationKeySize1024,
|
||||
pqCiphertextSize: mlkem.CiphertextSize1024,
|
||||
|
||||
pqNewPublicKey: func(data []byte) (crypto.Encapsulator, error) {
|
||||
return mlkem.NewEncapsulationKey1024(data)
|
||||
},
|
||||
pqNewPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.NewDecapsulationKey1024(data))
|
||||
},
|
||||
}
|
||||
|
||||
// MLKEM1024P384 returns a KEM implementing MLKEM1024-P384 from draft-ietf-hpke-pq.
|
||||
func MLKEM1024P384() KEM {
|
||||
return mlkem1024P384
|
||||
}
|
||||
|
||||
type hybridKEM struct {
|
||||
id uint16
|
||||
label string
|
||||
curve ecdh.Curve
|
||||
|
||||
curveSeedSize int
|
||||
curvePointSize int
|
||||
pqEncapsKeySize int
|
||||
pqCiphertextSize int
|
||||
|
||||
pqNewPublicKey func(data []byte) (crypto.Encapsulator, error)
|
||||
pqNewPrivateKey func(data []byte) (crypto.Decapsulator, error)
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) ID() uint16 {
|
||||
return kem.id
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) encSize() int {
|
||||
return kem.pqCiphertextSize + kem.curvePointSize
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) sharedSecret(ssPQ, ssT, ctT, ekT []byte) []byte {
|
||||
h := sha3.New256()
|
||||
h.Write(ssPQ)
|
||||
h.Write(ssT)
|
||||
h.Write(ctT)
|
||||
h.Write(ekT)
|
||||
h.Write([]byte(kem.label))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
type hybridPublicKey struct {
|
||||
kem *hybridKEM
|
||||
t *ecdh.PublicKey
|
||||
pq crypto.Encapsulator
|
||||
}
|
||||
|
||||
// NewHybridPublicKey returns a PublicKey implementing one of
|
||||
//
|
||||
// - MLKEM768-X25519 (a.k.a. X-Wing)
|
||||
// - MLKEM768-P256
|
||||
// - MLKEM1024-P384
|
||||
//
|
||||
// from draft-ietf-hpke-pq, depending on the underlying curve of t
|
||||
// ([ecdh.X25519], [ecdh.P256], or [ecdh.P384]) and the type of pq (either
|
||||
// *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
|
||||
//
|
||||
// This function is meant for applications that already have instantiated
|
||||
// crypto/ecdh and crypto/mlkem public keys. Otherwise, applications should use
|
||||
// the [KEM.NewPublicKey] method of e.g. [MLKEM768X25519].
|
||||
func NewHybridPublicKey(pq crypto.Encapsulator, t *ecdh.PublicKey) (PublicKey, error) {
|
||||
switch t.Curve() {
|
||||
case ecdh.X25519():
|
||||
if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
|
||||
return nil, errors.New("invalid PQ KEM for X25519 hybrid")
|
||||
}
|
||||
return &hybridPublicKey{mlkem768X25519, t, pq}, nil
|
||||
case ecdh.P256():
|
||||
if _, ok := pq.(*mlkem.EncapsulationKey768); !ok {
|
||||
return nil, errors.New("invalid PQ KEM for P-256 hybrid")
|
||||
}
|
||||
return &hybridPublicKey{mlkem768P256, t, pq}, nil
|
||||
case ecdh.P384():
|
||||
if _, ok := pq.(*mlkem.EncapsulationKey1024); !ok {
|
||||
return nil, errors.New("invalid PQ KEM for P-384 hybrid")
|
||||
}
|
||||
return &hybridPublicKey{mlkem1024P384, t, pq}, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) NewPublicKey(data []byte) (PublicKey, error) {
|
||||
if len(data) != kem.pqEncapsKeySize+kem.curvePointSize {
|
||||
return nil, errors.New("invalid public key size")
|
||||
}
|
||||
pq, err := kem.pqNewPublicKey(data[:kem.pqEncapsKeySize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k, err := kem.curve.NewPublicKey(data[kem.pqEncapsKeySize:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewHybridPublicKey(pq, k)
|
||||
}
|
||||
|
||||
func (pk *hybridPublicKey) KEM() KEM {
|
||||
return pk.kem
|
||||
}
|
||||
|
||||
func (pk *hybridPublicKey) Bytes() []byte {
|
||||
return append(pk.pq.Bytes(), pk.t.Bytes()...)
|
||||
}
|
||||
|
||||
var testingOnlyEncapsulate func() (ss, ct []byte)
|
||||
|
||||
func (pk *hybridPublicKey) encap() (sharedSecret []byte, encapPub []byte, err error) {
|
||||
skE, err := pk.t.Curve().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if testingOnlyGenerateKey != nil {
|
||||
skE = testingOnlyGenerateKey()
|
||||
}
|
||||
ssT, err := skE.ECDH(pk.t)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ctT := skE.PublicKey().Bytes()
|
||||
|
||||
ssPQ, ctPQ := pk.pq.Encapsulate()
|
||||
if testingOnlyEncapsulate != nil {
|
||||
ssPQ, ctPQ = testingOnlyEncapsulate()
|
||||
}
|
||||
|
||||
ss := pk.kem.sharedSecret(ssPQ, ssT, ctT, pk.t.Bytes())
|
||||
ct := append(ctPQ, ctT...)
|
||||
return ss, ct, nil
|
||||
}
|
||||
|
||||
type hybridPrivateKey struct {
|
||||
kem *hybridKEM
|
||||
seed []byte // can be nil
|
||||
t ecdh.KeyExchanger
|
||||
pq crypto.Decapsulator
|
||||
}
|
||||
|
||||
// NewHybridPrivateKey returns a PrivateKey implementing
|
||||
//
|
||||
// - MLKEM768-X25519 (a.k.a. X-Wing)
|
||||
// - MLKEM768-P256
|
||||
// - MLKEM1024-P384
|
||||
//
|
||||
// from draft-ietf-hpke-pq, depending on the underlying curve of t
|
||||
// ([ecdh.X25519], [ecdh.P256], or [ecdh.P384]) and the type of pq.Encapsulator()
|
||||
// (either *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
|
||||
//
|
||||
// This function is meant for applications that already have instantiated
|
||||
// crypto/ecdh and crypto/mlkem private keys, or another implementation of a
|
||||
// [ecdh.KeyExchanger] and [crypto.Decapsulator] (e.g. a hardware key).
|
||||
// Otherwise, applications should use the [KEM.NewPrivateKey] method of e.g.
|
||||
// [MLKEM768X25519].
|
||||
func NewHybridPrivateKey(pq crypto.Decapsulator, t ecdh.KeyExchanger) (PrivateKey, error) {
|
||||
return newHybridPrivateKey(pq, t, nil)
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) GenerateKey() (PrivateKey, error) {
|
||||
seed := make([]byte, 32)
|
||||
rand.Read(seed)
|
||||
return kem.NewPrivateKey(seed)
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) NewPrivateKey(priv []byte) (PrivateKey, error) {
|
||||
if len(priv) != 32 {
|
||||
return nil, errors.New("hpke: invalid hybrid KEM secret length")
|
||||
}
|
||||
|
||||
s := sha3.NewSHAKE256()
|
||||
s.Write(priv)
|
||||
|
||||
seedPQ := make([]byte, mlkem.SeedSize)
|
||||
s.Read(seedPQ)
|
||||
pq, err := kem.pqNewPrivateKey(seedPQ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seedT := make([]byte, kem.curveSeedSize)
|
||||
for {
|
||||
s.Read(seedT)
|
||||
k, err := kem.curve.NewPrivateKey(seedT)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return newHybridPrivateKey(pq, k, priv)
|
||||
}
|
||||
}
|
||||
|
||||
func newHybridPrivateKey(pq crypto.Decapsulator, t ecdh.KeyExchanger, seed []byte) (PrivateKey, error) {
|
||||
switch t.Curve() {
|
||||
case ecdh.X25519():
|
||||
if _, ok := pq.Encapsulator().(*mlkem.EncapsulationKey768); !ok {
|
||||
return nil, errors.New("invalid PQ KEM for X25519 hybrid")
|
||||
}
|
||||
return &hybridPrivateKey{mlkem768X25519, bytes.Clone(seed), t, pq}, nil
|
||||
case ecdh.P256():
|
||||
if _, ok := pq.Encapsulator().(*mlkem.EncapsulationKey768); !ok {
|
||||
return nil, errors.New("invalid PQ KEM for P-256 hybrid")
|
||||
}
|
||||
return &hybridPrivateKey{mlkem768P256, bytes.Clone(seed), t, pq}, nil
|
||||
case ecdh.P384():
|
||||
if _, ok := pq.Encapsulator().(*mlkem.EncapsulationKey1024); !ok {
|
||||
return nil, errors.New("invalid PQ KEM for P-384 hybrid")
|
||||
}
|
||||
return &hybridPrivateKey{mlkem1024P384, bytes.Clone(seed), t, pq}, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported curve")
|
||||
}
|
||||
}
|
||||
|
||||
func (kem *hybridKEM) DeriveKeyPair(ikm []byte) (PrivateKey, error) {
|
||||
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
|
||||
dk, err := SHAKE256().labeledDerive(suiteID, ikm, "DeriveKeyPair", nil, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kem.NewPrivateKey(dk)
|
||||
}
|
||||
|
||||
func (k *hybridPrivateKey) KEM() KEM {
|
||||
return k.kem
|
||||
}
|
||||
|
||||
func (k *hybridPrivateKey) Bytes() ([]byte, error) {
|
||||
if k.seed == nil {
|
||||
return nil, errors.New("private key seed not available")
|
||||
}
|
||||
return k.seed, nil
|
||||
}
|
||||
|
||||
func (k *hybridPrivateKey) PublicKey() PublicKey {
|
||||
return &hybridPublicKey{
|
||||
kem: k.kem,
|
||||
t: k.t.PublicKey(),
|
||||
pq: k.pq.Encapsulator(),
|
||||
}
|
||||
}
|
||||
|
||||
func (k *hybridPrivateKey) decap(enc []byte) ([]byte, error) {
|
||||
if len(enc) != k.kem.pqCiphertextSize+k.kem.curvePointSize {
|
||||
return nil, errors.New("invalid encapsulated key size")
|
||||
}
|
||||
ctPQ, ctT := enc[:k.kem.pqCiphertextSize], enc[k.kem.pqCiphertextSize:]
|
||||
ssPQ, err := k.pq.Decapsulate(ctPQ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pub, err := k.t.Curve().NewPublicKey(ctT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ssT, err := k.t.ECDH(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss := k.kem.sharedSecret(ssPQ, ssT, ctT, k.t.PublicKey().Bytes())
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
var mlkem768 = &mlkemKEM{
|
||||
id: 0x0041,
|
||||
ciphertextSize: mlkem.CiphertextSize768,
|
||||
newPublicKey: func(data []byte) (crypto.Encapsulator, error) {
|
||||
return mlkem.NewEncapsulationKey768(data)
|
||||
},
|
||||
newPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.NewDecapsulationKey768(data))
|
||||
},
|
||||
generateKey: func() (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.GenerateKey768())
|
||||
},
|
||||
}
|
||||
|
||||
// MLKEM768 returns a KEM implementing ML-KEM-768 from draft-ietf-hpke-pq.
|
||||
func MLKEM768() KEM {
|
||||
return mlkem768
|
||||
}
|
||||
|
||||
var mlkem1024 = &mlkemKEM{
|
||||
id: 0x0042,
|
||||
ciphertextSize: mlkem.CiphertextSize1024,
|
||||
newPublicKey: func(data []byte) (crypto.Encapsulator, error) {
|
||||
return mlkem.NewEncapsulationKey1024(data)
|
||||
},
|
||||
newPrivateKey: func(data []byte) (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.NewDecapsulationKey1024(data))
|
||||
},
|
||||
generateKey: func() (crypto.Decapsulator, error) {
|
||||
return wrapDecapsulator(mlkem.GenerateKey1024())
|
||||
},
|
||||
}
|
||||
|
||||
// MLKEM1024 returns a KEM implementing ML-KEM-1024 from draft-ietf-hpke-pq.
|
||||
func MLKEM1024() KEM {
|
||||
return mlkem1024
|
||||
}
|
||||
|
||||
type mlkemKEM struct {
|
||||
id uint16
|
||||
ciphertextSize int
|
||||
newPublicKey func(data []byte) (crypto.Encapsulator, error)
|
||||
newPrivateKey func(data []byte) (crypto.Decapsulator, error)
|
||||
generateKey func() (crypto.Decapsulator, error)
|
||||
}
|
||||
|
||||
func (kem *mlkemKEM) ID() uint16 {
|
||||
return kem.id
|
||||
}
|
||||
|
||||
func (kem *mlkemKEM) encSize() int {
|
||||
return kem.ciphertextSize
|
||||
}
|
||||
|
||||
type mlkemPublicKey struct {
|
||||
kem *mlkemKEM
|
||||
pq crypto.Encapsulator
|
||||
}
|
||||
|
||||
// NewMLKEMPublicKey returns a KEMPublicKey implementing
|
||||
//
|
||||
// - ML-KEM-768
|
||||
// - ML-KEM-1024
|
||||
//
|
||||
// from draft-ietf-hpke-pq, depending on the type of pub
|
||||
// (*[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
|
||||
//
|
||||
// This function is meant for applications that already have an instantiated
|
||||
// crypto/mlkem public key. Otherwise, applications should use the
|
||||
// [KEM.NewPublicKey] method of e.g. [MLKEM768].
|
||||
func NewMLKEMPublicKey(pub crypto.Encapsulator) (PublicKey, error) {
|
||||
switch pub.(type) {
|
||||
case *mlkem.EncapsulationKey768:
|
||||
return &mlkemPublicKey{mlkem768, pub}, nil
|
||||
case *mlkem.EncapsulationKey1024:
|
||||
return &mlkemPublicKey{mlkem1024, pub}, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported public key type")
|
||||
}
|
||||
}
|
||||
|
||||
func (kem *mlkemKEM) NewPublicKey(data []byte) (PublicKey, error) {
|
||||
pq, err := kem.newPublicKey(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewMLKEMPublicKey(pq)
|
||||
}
|
||||
|
||||
func (pk *mlkemPublicKey) KEM() KEM {
|
||||
return pk.kem
|
||||
}
|
||||
|
||||
func (pk *mlkemPublicKey) Bytes() []byte {
|
||||
return pk.pq.Bytes()
|
||||
}
|
||||
|
||||
func (pk *mlkemPublicKey) encap() (sharedSecret []byte, encapPub []byte, err error) {
|
||||
ss, ct := pk.pq.Encapsulate()
|
||||
if testingOnlyEncapsulate != nil {
|
||||
ss, ct = testingOnlyEncapsulate()
|
||||
}
|
||||
return ss, ct, nil
|
||||
}
|
||||
|
||||
type mlkemPrivateKey struct {
|
||||
kem *mlkemKEM
|
||||
pq crypto.Decapsulator
|
||||
}
|
||||
|
||||
// NewMLKEMPrivateKey returns a KEMPrivateKey implementing
|
||||
//
|
||||
// - ML-KEM-768
|
||||
// - ML-KEM-1024
|
||||
//
|
||||
// from draft-ietf-hpke-pq, depending on the type of priv.Encapsulator()
|
||||
// (either *[mlkem.EncapsulationKey768] or *[mlkem.EncapsulationKey1024]).
|
||||
//
|
||||
// This function is meant for applications that already have an instantiated
|
||||
// crypto/mlkem private key. Otherwise, applications should use the
|
||||
// [KEM.NewPrivateKey] method of e.g. [MLKEM768].
|
||||
func NewMLKEMPrivateKey(priv crypto.Decapsulator) (PrivateKey, error) {
|
||||
switch priv.Encapsulator().(type) {
|
||||
case *mlkem.EncapsulationKey768:
|
||||
return &mlkemPrivateKey{mlkem768, priv}, nil
|
||||
case *mlkem.EncapsulationKey1024:
|
||||
return &mlkemPrivateKey{mlkem1024, priv}, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported public key type")
|
||||
}
|
||||
}
|
||||
|
||||
func (kem *mlkemKEM) GenerateKey() (PrivateKey, error) {
|
||||
pq, err := kem.generateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewMLKEMPrivateKey(pq)
|
||||
}
|
||||
|
||||
func (kem *mlkemKEM) NewPrivateKey(priv []byte) (PrivateKey, error) {
|
||||
pq, err := kem.newPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewMLKEMPrivateKey(pq)
|
||||
}
|
||||
|
||||
func (kem *mlkemKEM) DeriveKeyPair(ikm []byte) (PrivateKey, error) {
|
||||
suiteID := byteorder.BEAppendUint16([]byte("KEM"), kem.id)
|
||||
dk, err := SHAKE256().labeledDerive(suiteID, ikm, "DeriveKeyPair", nil, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kem.NewPrivateKey(dk)
|
||||
}
|
||||
|
||||
func (k *mlkemPrivateKey) KEM() KEM {
|
||||
return k.kem
|
||||
}
|
||||
|
||||
func (k *mlkemPrivateKey) Bytes() ([]byte, error) {
|
||||
pq, ok := k.pq.(interface {
|
||||
Bytes() []byte
|
||||
})
|
||||
if !ok {
|
||||
return nil, errors.New("private key seed not available")
|
||||
}
|
||||
return pq.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *mlkemPrivateKey) PublicKey() PublicKey {
|
||||
return &mlkemPublicKey{
|
||||
kem: k.kem,
|
||||
pq: k.pq.Encapsulator(),
|
||||
}
|
||||
}
|
||||
|
||||
func (k *mlkemPrivateKey) decap(enc []byte) ([]byte, error) {
|
||||
return k.pq.Decapsulate(enc)
|
||||
}
|
||||
|
||||
func wrapDecapsulator(dk any, err error) (crypto.Decapsulator, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch key := dk.(type) {
|
||||
case *mlkem.DecapsulationKey768:
|
||||
return crypto.DecapsulatorFromDecapsulationKey768(key), nil
|
||||
case *mlkem.DecapsulationKey1024:
|
||||
return crypto.DecapsulatorFromDecapsulationKey1024(key), nil
|
||||
default:
|
||||
return nil, errors.New("hpke: internal error: unknown decapsulation key type")
|
||||
}
|
||||
}
|
||||
+879
-1850
File diff suppressed because it is too large.
Load diff
+796
-1572
File diff suppressed because it is too large.
Load diff
+5
-1
@@ -111,8 +111,12 @@ func (t TokenSlice) WordbreakPrefix() string {
|
||||
break
|
||||
}
|
||||
|
||||
if token.Type == WORDBREAK_TOKEN {
|
||||
if !found && token.Type == WORDBREAK_TOKEN {
|
||||
found = true
|
||||
if token.Value == "@" {
|
||||
// Seems although `@` is a wordbreak, it weirdly is not part of the prefix.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ package shlex
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const BASH_WORDBREAKS = " \t\r\n" + `"'><=;|&(:`
|
||||
const BASH_WORDBREAKS = " \t\r\n" + `"'@><=;|&(:`
|
||||
|
||||
type WordbreakType int
|
||||
|
||||
|
||||
+27
-3
@@ -437,6 +437,15 @@ func (a Action) Timeout(d time.Duration, alternative Action) Action {
|
||||
})
|
||||
}
|
||||
|
||||
// Unique ensures the Action only contains unique values.
|
||||
func (a Action) Unique() Action {
|
||||
return ActionCallback(func(c Context) Action {
|
||||
invoked := a.Invoke(c)
|
||||
invoked.action.rawValues = invoked.action.rawValues.Unique()
|
||||
return invoked.ToA()
|
||||
})
|
||||
}
|
||||
|
||||
// UniqueList wraps the Action in an ActionMultiParts with given divider.
|
||||
func (a Action) UniqueList(divider string) Action {
|
||||
return ActionMultiParts(divider, func(c Context) Action {
|
||||
@@ -513,9 +522,14 @@ func (a Action) UidF(f func(s string, uc uid.Context) (*url.URL, error)) Action
|
||||
}
|
||||
|
||||
// Usage sets the usage.
|
||||
func (a Action) Usage(usage string, args ...interface{}) Action {
|
||||
func (a Action) Usage(usage string, args ...any) Action {
|
||||
return a.UsageF(func() string {
|
||||
return fmt.Sprintf(usage, args...)
|
||||
if len(args) == 0 {
|
||||
return usage
|
||||
}
|
||||
return func(u string, a ...any) string { // avoids failed inferred printf check: non-constant format string
|
||||
return fmt.Sprintf(u, a...)
|
||||
}(usage, args...)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -548,6 +562,9 @@ func (a Action) Query(scheme, host, path string, opts ...string) Action {
|
||||
values.Set(opts[i], opts[i+1])
|
||||
}
|
||||
}
|
||||
if c.Value != "" {
|
||||
values.Set("C_VALUE", c.Value)
|
||||
}
|
||||
query.RawQuery = values.Encode()
|
||||
}
|
||||
a.meta.Queries.Add(query.String())
|
||||
@@ -558,10 +575,17 @@ func (a Action) Query(scheme, host, path string, opts ...string) Action {
|
||||
// QueryF TODO experimental
|
||||
func (a Action) QueryF(f func(s string, uc uid.Context) (*url.URL, error)) Action { // TODO remove the string
|
||||
return ActionCallback(func(c Context) Action {
|
||||
query, err := f(c.Value, c)
|
||||
query, err := f("", c) // TODO string parameter from uid isn't really needed here
|
||||
if err != nil {
|
||||
return ActionMessage(err.Error())
|
||||
}
|
||||
|
||||
if c.Value != "" {
|
||||
values := query.Query()
|
||||
values.Set("C_VALUE", c.Value)
|
||||
query.RawQuery = values.Encode()
|
||||
}
|
||||
|
||||
a.meta.Queries.Add(query.String())
|
||||
return a
|
||||
})
|
||||
|
||||
+3
-4
@@ -2,6 +2,7 @@
|
||||
package carapace
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"os"
|
||||
|
||||
"github.com/carapace-sh/carapace/internal/shell"
|
||||
@@ -79,9 +80,7 @@ func (c Carapace) FlagCompletion(actions ActionMap) {
|
||||
if e.flag == nil {
|
||||
e.flag = actions
|
||||
} else {
|
||||
for name, action := range actions {
|
||||
e.flag[name] = action
|
||||
}
|
||||
maps.Copy(e.flag, actions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +125,7 @@ func IsCallback() bool {
|
||||
// func TestCarapace(t *testing.T) {
|
||||
// carapace.Test(t)
|
||||
// }
|
||||
func Test(t interface{ Error(args ...interface{}) }) {
|
||||
func Test(t interface{ Error(args ...any) }) {
|
||||
for _, e := range storage.check() {
|
||||
t.Error(e)
|
||||
}
|
||||
|
||||
+20
-32
@@ -8,6 +8,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/carapace-sh/carapace/internal/common"
|
||||
@@ -135,6 +136,10 @@ func ActionExecute(cmd *cobra.Command) Action {
|
||||
// ActionDirectories completes directories.
|
||||
func ActionDirectories() Action {
|
||||
return ActionCallback(func(c Context) Action {
|
||||
cDir, err := c.Abs(c.Dir)
|
||||
if err != nil {
|
||||
return ActionMessage(err.Error())
|
||||
}
|
||||
return actionPath([]string{""}, true).
|
||||
MultiParts("/").
|
||||
StyleF(style.ForPath).
|
||||
@@ -145,19 +150,17 @@ func ActionDirectories() Action {
|
||||
}
|
||||
return url.Parse("file://" + uid.PathEscape(abs))
|
||||
}).
|
||||
QueryF(func(s string, uc uid.Context) (*url.URL, error) {
|
||||
abs, err := c.Abs(c.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return url.Parse(fmt.Sprintf("file://%v?directories=true", uid.PathEscape(abs)))
|
||||
})
|
||||
Query("file", "directories", "", "C_DIR", cDir)
|
||||
}).Tag("directories")
|
||||
}
|
||||
|
||||
// ActionFiles completes files with optional suffix filtering.
|
||||
func ActionFiles(suffix ...string) Action {
|
||||
return ActionCallback(func(c Context) Action {
|
||||
cDir, err := c.Abs(c.Dir)
|
||||
if err != nil {
|
||||
return ActionMessage(err.Error())
|
||||
}
|
||||
return actionPath(suffix, false).
|
||||
MultiParts("/").
|
||||
StyleF(style.ForPath).
|
||||
@@ -168,23 +171,9 @@ func ActionFiles(suffix ...string) Action {
|
||||
}
|
||||
return url.Parse("file://" + uid.PathEscape(abs))
|
||||
}).
|
||||
QueryF(func(s string, uc uid.Context) (*url.URL, error) {
|
||||
abs, err := c.Abs(c.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := &url.URL{
|
||||
Scheme: "file",
|
||||
Path: abs,
|
||||
}
|
||||
if len(suffix) > 0 {
|
||||
values := query.Query()
|
||||
values.Set("suffix", strings.Join(suffix, ","))
|
||||
query.RawQuery = values.Encode()
|
||||
}
|
||||
return query, nil
|
||||
})
|
||||
// Query("file", "", c.Dir, "suffix", strings.Join(suffix, ","))
|
||||
Query("file", "files", "",
|
||||
"C_DIR", cDir,
|
||||
"suffix", strings.Join(suffix, ","))
|
||||
}).Tag("files")
|
||||
}
|
||||
|
||||
@@ -247,7 +236,7 @@ func ActionStyledValuesDescribed(values ...string) Action {
|
||||
}
|
||||
|
||||
// ActionMessage displays a help messages in places where no completions can be generated.
|
||||
func ActionMessage(msg string, args ...interface{}) Action {
|
||||
func ActionMessage(msg string, args ...any) Action {
|
||||
return ActionCallback(func(c Context) Action {
|
||||
if len(args) > 0 {
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
@@ -477,14 +466,15 @@ func ActionExecutables(dirs ...string) Action {
|
||||
}
|
||||
manDescriptions := man.Descriptions(c.Value) // TODO allow additional descriptions to be registered somewhere for carapace-bin (key, value,...)
|
||||
batch := Batch()
|
||||
for i := len(dirs) - 1; i >= 0; i-- {
|
||||
batch = append(batch, actionDirectoryExecutables(dirs[i], c.Value, manDescriptions))
|
||||
for _, dir := range dirs {
|
||||
batch = append(batch, actionDirectoryExecutables(dir, c.Value, manDescriptions))
|
||||
}
|
||||
return batch.ToA().
|
||||
UidF(func(s string, uc uid.Context) (*url.URL, error) {
|
||||
return &url.URL{Scheme: "cmd", Host: uid.PathEscape(s)}, nil
|
||||
}).
|
||||
Query("cmd", "", "", "directories", strings.Join(dirs, ", "))
|
||||
Query("cmd", "", "", "directories", strings.Join(dirs, ", ")).
|
||||
Unique()
|
||||
}).Tag("executables")
|
||||
}
|
||||
|
||||
@@ -553,10 +543,8 @@ func ActionCommands(cmd *cobra.Command) Action {
|
||||
return ActionCallback(func(c Context) Action {
|
||||
if len(c.Args) > 0 {
|
||||
for _, subCommand := range cmd.Commands() {
|
||||
for _, name := range append(subCommand.Aliases, subCommand.Name()) {
|
||||
if name == c.Args[0] { // cmd.Find is too lenient
|
||||
return ActionCommands(subCommand).Shift(1)
|
||||
}
|
||||
if slices.Contains(append(subCommand.Aliases, subCommand.Name()), c.Args[0]) { // cmd.Find is too lenient
|
||||
return ActionCommands(subCommand).Shift(1)
|
||||
}
|
||||
}
|
||||
return ActionMessage("unknown subcommand %#v for %#v", c.Args[0], cmd.Name())
|
||||
|
||||
+26
@@ -1,6 +1,8 @@
|
||||
package carapace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/carapace-sh/carapace/internal/common"
|
||||
"github.com/carapace-sh/carapace/pkg/style"
|
||||
)
|
||||
@@ -42,3 +44,27 @@ func Diff(original, new Action) Action {
|
||||
return mergedBatch.ToA()
|
||||
})
|
||||
}
|
||||
|
||||
// TODO experimental - needs different signature
|
||||
// TODO return patch format
|
||||
func DiffPatch(original, new Action, c Context) []string {
|
||||
diff := Diff(original, new).Invoke(c)
|
||||
patch := make([]string, 0)
|
||||
for _, v := range diff.action.rawValues {
|
||||
s := v.Value
|
||||
if v.Description != "" {
|
||||
s += fmt.Sprintf(" (%v)", v.Description)
|
||||
}
|
||||
|
||||
switch v.Style {
|
||||
case style.Red:
|
||||
s = "- " + s
|
||||
case style.Green:
|
||||
s = "+ " + s
|
||||
default:
|
||||
s = " " + s
|
||||
}
|
||||
patch = append(patch, s)
|
||||
}
|
||||
return patch
|
||||
}
|
||||
Loaded 100 of 511 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user