package maintenance import ( "context" "crypto/ecdh" "encoding/base64" "errors" "io" "os" "time" ) const MaxResultBytes = 64 << 10 const MaxExecutionDuration = 10 * time.Minute var ( ErrResult = errors.New("maintenance: private result unavailable") ErrResultOverflow = errors.New("maintenance: private result exceeds limit") ) // CaptureResult takes ownership of a dedicated stdout read pipe and closes it // on every return. It returns complete bytes only after EOF within the deadline. // Closing this pipe does not prove the command or its descendants have stopped; // the worker must separately check Wait and containment before publishing success. func CaptureResult(ctx context.Context, pipe *os.File) ([]byte, error) { if pipe == nil { return nil, ErrResult } defer pipe.Close() if ctx == nil { return nil, ErrResult } deadline, ok := ctx.Deadline() remaining := time.Until(deadline) if !ok || remaining <= 0 || remaining > MaxExecutionDuration || ctx.Err() != nil { return nil, ErrResult } info, err := pipe.Stat() if err != nil || info.Mode()&os.ModeNamedPipe == 0 { return nil, ErrResult } stop := context.AfterFunc(ctx, func() { _ = pipe.Close() }) defer stop() body, err := io.ReadAll(io.LimitReader(pipe, MaxResultBytes+1)) if err != nil || ctx.Err() != nil { clear(body) return nil, ErrResult } if len(body) > MaxResultBytes { clear(body) return nil, ErrResultOverflow } if err := pipe.Close(); err != nil || ctx.Err() != nil { clear(body) return nil, ErrResult } return body, nil } func SealResult(pub *ecdh.PublicKey, binding Binding, body []byte) (string, error) { if binding.Direction != DirectionResult || len(body) > MaxResultBytes { return "", ErrResult } sealed, err := Seal(pub, binding, body) if err != nil { return "", ErrResult } return sealed, nil } // OpenResult decrypts bytes for the recipient. Encryption alone does not prove // which worker produced them; callers must verify the authenticated operation // outcome before treating these bytes as execution or completion evidence. func OpenResult(priv *ecdh.PrivateKey, binding Binding, sealed string) ([]byte, error) { const overhead = len(envelopePrefix) + ephemeralBytes + nonceBytes + tagBytes if binding.Direction != DirectionResult || len(sealed) > base64.StdEncoding.EncodedLen(overhead+MaxResultBytes) { return nil, ErrResult } body, err := Open(priv, binding, sealed) if err != nil { return nil, ErrResult } // The decoded bound is necessary even after the encoded-length check: // base64 padding can give adjacent raw lengths the same encoded length. if len(body) > MaxResultBytes { clear(body) return nil, ErrResult } return body, nil }