Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 42 additions & 37 deletions otdfctl/cmd/tdf/decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package tdf

import (
"errors"
"fmt"
"io"
"os"

"github.com/opentdf/platform/lib/ocrypto"
"github.com/opentdf/platform/otdfctl/cmd/common"
"github.com/opentdf/platform/otdfctl/pkg/cli"
"github.com/opentdf/platform/otdfctl/pkg/handlers"
"github.com/opentdf/platform/otdfctl/pkg/man"
"github.com/opentdf/platform/otdfctl/pkg/utils"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -43,55 +43,60 @@ func decryptRun(cmd *cobra.Command, args []string) {
sessionKeyAlgorithm = ocrypto.RSA2048Key
}

// check for piped input
piped := readPipedStdin()

// Prefer file argument over piped input over default filename
bytesToDecrypt := piped
// Prefer the file argument over piped input.
var tdfFile string
var err error
if len(args) > 0 {
tdfFile = args[0]
bytesToDecrypt, err = utils.ReadBytesFromFile(tdfFile, MaxFileSize)
}
in, closeIn, err := openSeekableInput(tdfFile)
if errors.Is(err, errNoInput) {
cli.ExitWithError("Must provide ONE of the following to decrypt: [file argument, stdin input]", err)
} else if err != nil {
cli.ExitWithError("Failed to read file:", err)
}
defer closeIn()

// Resolve the destination before decrypting, so the plaintext streams
// straight to it rather than accumulating in memory first.
var dest io.Writer = os.Stdout
var outFile *outputFile
if output != "" {
outFile, err = newOutputFile(output)
if err != nil {
cli.ExitWithError("Failed to read file:", err)
closeIn()
cli.ExitWithError("Failed to write decrypted data to file", err)
}
defer outFile.Cleanup()
dest = outFile
}

if len(bytesToDecrypt) == 0 {
cli.ExitWithError("Must provide ONE of the following to decrypt: [file argument, stdin input]", errors.New("no input provided"))
// cli.ExitWithError calls os.Exit, which skips deferred functions, so both
// the spooled input and the partial output have to be discarded first.
fail := func(msg string, err error) {
closeIn()
if outFile != nil {
outFile.Cleanup()
}
cli.ExitWithError(msg, err)
Comment on lines +73 to +80

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmihalcik-virtru can you move this into the helper? There are other cases where we pipe results through and it seems like this should be reusable.

}

ignoreAllowlist := len(kasAllowList) == 1 && kasAllowList[0] == "*"

decrypted, err := h.DecryptBytes(
c.Context(),
bytesToDecrypt,
assertionVerification,
disableAssertionVerification,
sessionKeyAlgorithm,
kasAllowList,
ignoreAllowlist,
nil,
)
err = h.Decrypt(c.Context(), dest, in, handlers.DecryptOptions{
AssertionVerificationKeysFile: assertionVerification,
DisableAssertionCheck: disableAssertionVerification,
SessionKeyAlgorithm: sessionKeyAlgorithm,
KASAllowList: kasAllowList,
IgnoreAllowlist: ignoreAllowlist,
})
if err != nil {
cli.ExitWithError("Failed to decrypt file", err)
fail("Failed to decrypt file", err)
}

if output == "" {
//nolint:forbidigo // printing decrypted content to stdout
fmt.Print(decrypted.String())
return
}
// Here 'output' is the filename given with -o
f, err := os.Create(output)
if err != nil {
cli.ExitWithError("Failed to write decrypted data to file", err)
}
defer f.Close()
_, err = f.Write(decrypted.Bytes())
if err != nil {
cli.ExitWithError("Failed to write decrypted data to file", err)
if outFile != nil {
if err := outFile.Commit(); err != nil {
fail("Failed to write decrypted data to file", err)
}
}
}

Expand Down
128 changes: 89 additions & 39 deletions otdfctl/cmd/tdf/encrypt.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package tdf

import (
"bufio"
"errors"
"io"
"log/slog"
"os"
Expand All @@ -11,8 +13,8 @@ import (
"github.com/opentdf/platform/lib/ocrypto"
"github.com/opentdf/platform/otdfctl/cmd/common"
"github.com/opentdf/platform/otdfctl/pkg/cli"
"github.com/opentdf/platform/otdfctl/pkg/handlers"
"github.com/opentdf/platform/otdfctl/pkg/man"
"github.com/opentdf/platform/otdfctl/pkg/utils"
"github.com/spf13/cobra"
)

Expand All @@ -24,6 +26,38 @@ var (
EncryptCmd = &encryptDoc.Command
)

// detectMimeType sniffs the payload's type from its head, and returns a reader
// that still yields the whole payload.
//
// Detection needs only the first megabyte, and Peek does not consume, so those
// bytes stay in the buffer and reach the encoder. The payload is never held in
// memory in full. Wrapping is uniform across files and pipes and needs no seek.
func detectMimeType(in io.Reader, fileExt string) (string, io.Reader, error) {
buffered, ok := in.(*bufio.Reader)
if !ok {
buffered = bufio.NewReaderSize(in, Size1MB)
in = buffered
}

mimetype.SetLimit(Size1MB) // limit to 1MB
head, err := buffered.Peek(Size1MB)
// A payload shorter than the peek window is the common case, not an error.
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, bufio.ErrBufferFull) {
return "", in, err
}

// defaults to application/octet-stream if nothing is recognized
detected := mimetype.Detect(head).String()
if detected == "application/octet-stream" && fileExt != "" {
// Lookup returns nil for an extension it does not know, which is not an
// error — octet-stream remains the right answer.
if byExt := mimetype.Lookup(fileExt); byExt != nil {
detected = byExt.String()
}
}
return detected, in, nil
}
Comment on lines +29 to +59

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be moved to a helper library rather than embedded in the command?


func encryptRun(cmd *cobra.Command, args []string) {
c := cli.New(cmd, args, cli.WithPrintJSON())
h := common.NewHandler(c)
Expand Down Expand Up @@ -57,13 +91,13 @@ func encryptRun(cmd *cobra.Command, args []string) {
wrappingKeyAlgorithm = ocrypto.RSA2048Key
}

piped := readPipedStdin()
piped, hasPiped := stdinReader()

inputCount := 0
if filePath != "" {
inputCount++
}
if len(piped) > 0 {
if hasPiped {
inputCount++
}

Expand All @@ -77,70 +111,86 @@ func encryptRun(cmd *cobra.Command, args []string) {
}

// prefer filepath argument over stdin input
bytesSlice := piped
var err error
var in io.Reader = piped
inputSize := handlers.InputSizeUnknown
if filePath != "" {
bytesSlice, err = utils.ReadBytesFromFile(filePath, MaxFileSize)
f, err := os.Open(filePath)
if err != nil {
cli.ExitWithError("Failed to read file:", err)
}
defer f.Close()
// Declaring the length keeps the SDK off its unknown-size path, which
// must assume ZIP64 because the choice is baked into the header of the
// first segment. Without this, wrapping the file for MIME detection
// below would hide its Seeker and silently change the output format.
if fi, err := f.Stat(); err == nil && fi.Mode().IsRegular() {
inputSize = fi.Size()
}
in = f
}

// auto-detect mime type if not provided
if fileMimeType == "" {
slog.Debug("detecting mime type of file")
// get the mime type of the file
mimetype.SetLimit(Size1MB) // limit to 1MB
m := mimetype.Detect(bytesSlice)
// default to application/octet-stream if no mime type is detected
fileMimeType = m.String()

if fileMimeType == "application/octet-stream" {
if fileExt != "" {
fileMimeType = mimetype.Lookup(fileExt).String()
}
var err error
fileMimeType, in, err = detectMimeType(in, fileExt)
if err != nil {
cli.ExitWithError("Failed to read file:", err)
}
}
slog.Debug("encrypting file",
slog.Int("file_len", len(bytesSlice)),
slog.Int64("file_len", inputSize),
slog.String("mime_type", fileMimeType),
)

// Do the encryption
encrypted, err := h.EncryptBytes(
tdfType,
bytesSlice,
attrValues,
fileMimeType,
kasURLPath,
assertions,
wrappingKeyAlgorithm,
targetMode,
)
if err != nil {
cli.ExitWithError("Failed to encrypt", err)
}

// Find the destination as the output flag filename or stdout
var dest *os.File
// Resolve the destination before encrypting, so that the payload streams
// straight to it rather than accumulating in memory first.
var dest io.Writer
var tdfFile *outputFile
if out != "" {
// make sure output ends in .tdf extension
if !strings.HasSuffix(out, ".tdf") {
out += ".tdf"
}
tdfFile, err := os.Create(out)
var err error
tdfFile, err = newOutputFile(out)
if err != nil {
cli.ExitWithError("Failed to write encrypted file "+out, err)
}
defer tdfFile.Close()
defer tdfFile.Cleanup()
dest = tdfFile
} else {
dest = os.Stdout
}

_, e := io.Copy(dest, encrypted)
if e != nil {
cli.ExitWithError("Failed to write encrypted data to stdout", e)
// cli.ExitWithError calls os.Exit, which skips deferred functions, so the
// partial output has to be discarded before handing off to it.
fail := func(msg string, err error) {
if tdfFile != nil {
tdfFile.Cleanup()
}
cli.ExitWithError(msg, err)
}
Comment on lines +166 to +173

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same callout as in the decrypt.


// Do the encryption
err := h.Encrypt(c.Context(), dest, in, handlers.EncryptOptions{
TDFType: tdfType,
Attributes: attrValues,
MimeType: fileMimeType,
KASURLPath: kasURLPath,
Assertions: assertions,
WrappingKeyAlgorithm: wrappingKeyAlgorithm,
TargetMode: targetMode,
InputSize: inputSize,
})
if err != nil {
fail("Failed to encrypt", err)
}

if tdfFile != nil {
if err := tdfFile.Commit(); err != nil {
fail("Failed to write encrypted file "+out, err)
}
}
}

Expand Down
30 changes: 23 additions & 7 deletions otdfctl/cmd/tdf/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,32 @@ func inspectRun(cmd *cobra.Command, args []string) {
h := common.NewHandler(c)
defer h.Close()

data := cli.ReadFromArgsOrPipe(args, nil)
if len(data) == 0 {
c.ExitWithError("must provide ONE of the following: [file argument, stdin input]", errors.New("no input provided"))
// Inspect reads only the manifest, so the TDF is never held in memory even
// when the payload dwarfs it.
var path string
if len(args) > 0 {
path = args[0]
}
in, closeIn, err := openSeekableInput(path)
if errors.Is(err, errNoInput) {
c.ExitWithError("must provide ONE of the following: [file argument, stdin input]", err)
} else if err != nil {
c.ExitWithError("failed to read input", err)
}
// Every exit below goes through os.Exit, which skips deferred functions, so
// a spooled pipe has to be discarded explicitly on each path.
defer closeIn()
fail := func(msg string, err error) {
closeIn()
c.ExitWithError(msg, err)
}

result, errs := h.InspectTDF(data)
result, errs := h.InspectTDF(in)
for _, err := range errs {
if errors.Is(err, handlers.ErrTDFInspectFailNotValidTDF) {
c.ExitWithError("not a valid TDF", err)
fail("not a valid TDF", err)
} else if errors.Is(err, handlers.ErrTDFInspectFailNotInspectable) {
c.ExitWithError("failed to inspect TDF", err)
fail("failed to inspect TDF", err)
}
}

Expand All @@ -76,9 +91,10 @@ func inspectRun(cmd *cobra.Command, args []string) {
Attributes: result.Attributes,
}

closeIn()
c.ExitWithJSON(m, cli.ExitCodeSuccess)
}
c.ExitWithError("failed to inspect TDF", nil)
fail("failed to inspect TDF", nil)
}

func InitInspectCommand() {
Expand Down
Loading
Loading