github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/libkb/stream_classifier.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package libkb
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"encoding/base64"
    10  	"io"
    11  	"strings"
    12  
    13  	"github.com/keybase/go-codec/codec"
    14  	"github.com/keybase/saltpack"
    15  )
    16  
    17  // CryptoMessageFormat is one of the known crypto message formats that we admit
    18  type CryptoMessageFormat string
    19  
    20  const (
    21  	// CryptoMessageFormatPGP is for PGP
    22  	CryptoMessageFormatPGP CryptoMessageFormat = "pgp"
    23  	// CryptoMessageFormatKeybaseV0 is for the zeroth version of Keybase signatures, which
    24  	// will eventually be deprecated.
    25  	CryptoMessageFormatKeybaseV0 CryptoMessageFormat = "kbv0"
    26  	// CryptoMessageFormatSaltpack is the Saltpack messaging format for encrypted and signed
    27  	// messages
    28  	CryptoMessageFormatSaltpack CryptoMessageFormat = "saltpack"
    29  )
    30  
    31  // CryptoMessageType says what type of crypto message it is, regardless of Format
    32  type CryptoMessageType int
    33  
    34  const (
    35  	// CryptoMessageTypeEncryption is for an encrypted message
    36  	CryptoMessageTypeEncryption CryptoMessageType = 0
    37  	// CryptoMessageTypeAttachedSignature is for an attached signature
    38  	CryptoMessageTypeAttachedSignature CryptoMessageType = 1
    39  	// CryptoMessageTypeDetachedSignature is for a detached signature
    40  	CryptoMessageTypeDetachedSignature CryptoMessageType = 2
    41  	// CryptoMessageTypeClearSignature is for PGP clearsigning
    42  	CryptoMessageTypeClearSignature CryptoMessageType = 3
    43  	// CryptoMessageTypeAmbiguous is for an ambiguous message based on the stream prefix
    44  	CryptoMessageTypeAmbiguous CryptoMessageType = 4
    45  	// CryptoMessageTypeSignature is for a sig that can be either attached or detached
    46  	CryptoMessageTypeSignature CryptoMessageType = 5
    47  )
    48  
    49  // StreamClassification tells what Format the stream is, if it's a Public signature or a Private
    50  // Message, if it's a detached or attached signature in the public case, and if it's
    51  // armored or binary.
    52  type StreamClassification struct {
    53  	Format  CryptoMessageFormat
    54  	Type    CryptoMessageType
    55  	Armored bool
    56  }
    57  
    58  func isBase64KeybaseV0Sig(s string) bool {
    59  	firstKey := "body"
    60  	dataBytes := len(firstKey) + 1
    61  	b64dataBytes := (dataBytes + 1) * 4 / 3
    62  	if len(s) < b64dataBytes {
    63  		return false
    64  	}
    65  	buf, err := base64.StdEncoding.DecodeString(s[0:b64dataBytes])
    66  	if err != nil {
    67  		return false
    68  	}
    69  	// If there is a way to feed base64-like data thats longer than that
    70  	// b64dataBytes but yields an empty buffer, bail out and not crash later.
    71  	if len(buf) == 0 {
    72  		return false
    73  	}
    74  	// Packet should be an encoded dictionary of 3 values
    75  	if buf[0] != 0x83 {
    76  		return false
    77  	}
    78  	var mh codec.MsgpackHandle
    79  	var encoded []byte
    80  	_ = codec.NewEncoderBytes(&encoded, &mh).Encode(firstKey)
    81  	return bytes.HasPrefix(buf[1:], encoded)
    82  }
    83  
    84  func isSaltpackMessage(stream *bufio.Reader, sc *StreamClassification) bool {
    85  	isArmored, _, messageType, _, err := saltpack.ClassifyStream(stream)
    86  	if err != nil {
    87  		return false
    88  	}
    89  
    90  	sc.Armored = isArmored
    91  
    92  	switch messageType {
    93  	case saltpack.MessageTypeEncryption, saltpack.MessageTypeSigncryption:
    94  		sc.Type = CryptoMessageTypeEncryption
    95  	case saltpack.MessageTypeAttachedSignature:
    96  		sc.Type = CryptoMessageTypeAttachedSignature
    97  	case saltpack.MessageTypeDetachedSignature:
    98  		sc.Type = CryptoMessageTypeDetachedSignature
    99  	default:
   100  		return false
   101  	}
   102  	sc.Format = CryptoMessageFormatSaltpack
   103  	return true
   104  }
   105  
   106  func isPGPBinary(b []byte, sc *StreamClassification) bool {
   107  	if len(b) < 2 {
   108  		return false
   109  	}
   110  	// Top bit is set on PGP packets
   111  	if b[0]&0x80 == 0 {
   112  		return false
   113  	}
   114  
   115  	var tag byte
   116  	if (b[0] & 0x40) == 0 {
   117  		// "Old"-style Tag Format
   118  		tag = (b[0] & 0x3f) >> 2
   119  	} else {
   120  		// "New"-style Tag Format
   121  		tag = (b[0] & 0x3f)
   122  	}
   123  	switch tag {
   124  	case 0x1:
   125  		// Encrypted session Key
   126  		sc.Type = CryptoMessageTypeEncryption
   127  	case 0x2:
   128  		// Detached signature
   129  		sc.Type = CryptoMessageTypeDetachedSignature
   130  	case 0x4, 0x8:
   131  		// Either a compressed message or just a one-pass signature type. In either case,
   132  		// it's likely a signature.
   133  		sc.Type = CryptoMessageTypeAttachedSignature
   134  	default:
   135  		return false
   136  	}
   137  	sc.Format = CryptoMessageFormatPGP
   138  	return true
   139  }
   140  
   141  func isUTF16Mark(b []byte) bool {
   142  	if len(b) < 2 {
   143  		return false
   144  	}
   145  	return ((b[0] == 0xFE && b[1] == 0xFF) || (b[0] == 0xFF && b[1] == 0xFE))
   146  }
   147  
   148  // ClassifyStream takes a stream reader in, and returns a likely classification
   149  // of that stream without consuming any data from it. It returns a reader that you
   150  // should read from instead, in addition to the classification. If classification
   151  // fails, there will be a `UnknownStreamError`, or additional EOF errors if the
   152  // stream ended before classification could go.
   153  func ClassifyStream(r io.Reader) (sc StreamClassification, out io.Reader, err error) {
   154  	// 4096 is currently the default buffer size. It is specified explicitly because go 1.9 does not
   155  	// expose the size of a bufio.Reader (go 1.10 does).
   156  	stream := bufio.NewReaderSize(r, 4096)
   157  
   158  	buf, err := stream.Peek(4096)
   159  	if err != nil {
   160  		// If we had a short peek (for example, due to a short stream), we can still continue and ignore the error
   161  		if len(buf) == 0 {
   162  			return sc, stream, err
   163  		}
   164  		err = nil
   165  	}
   166  
   167  	sb := string(buf)
   168  	switch {
   169  	case strings.HasPrefix(sb, "-----BEGIN PGP MESSAGE-----"):
   170  		sc.Format = CryptoMessageFormatPGP
   171  		sc.Armored = true
   172  		sc.Type = CryptoMessageTypeAmbiguous
   173  	case strings.HasPrefix(sb, "-----BEGIN PGP SIGNATURE-----"):
   174  		sc.Format = CryptoMessageFormatPGP
   175  		sc.Armored = true
   176  		sc.Type = CryptoMessageTypeDetachedSignature
   177  	case strings.HasPrefix(sb, "-----BEGIN PGP SIGNED MESSAGE-----"):
   178  		sc.Format = CryptoMessageFormatPGP
   179  		sc.Armored = true
   180  		sc.Type = CryptoMessageTypeClearSignature
   181  	case isSaltpackMessage(stream, &sc):
   182  		// Format etc. set by isSaltpackBinary().
   183  	case isBase64KeybaseV0Sig(sb):
   184  		sc.Format = CryptoMessageFormatKeybaseV0
   185  		sc.Armored = true
   186  		sc.Type = CryptoMessageTypeAttachedSignature
   187  	case isPGPBinary(buf, &sc):
   188  		// Format etc. set by isPGPBinary().
   189  	case isUTF16Mark(buf):
   190  		err = UTF16UnsupportedError{}
   191  	default:
   192  		err = UnknownStreamError{}
   193  	}
   194  	return sc, stream, err
   195  }