git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/zign/sign.go (about)

     1  package zign
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/binary"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  
    12  	"git.sr.ht/~pingoo/stdx/crypto"
    13  	"github.com/zeebo/blake3"
    14  )
    15  
    16  type SignInput struct {
    17  	Filename string
    18  	Reader   io.Reader
    19  }
    20  
    21  type SignOutput struct {
    22  	Filename   string `json:"file"`
    23  	HashBlake3 string `json:"hash_blake3"`
    24  	Signature  []byte `json:"signature"`
    25  }
    26  
    27  func Sign(encryptedBase64PrivateKey string, password string, input SignInput) (output SignOutput, err error) {
    28  	res, err := SignMany(encryptedBase64PrivateKey, password, []SignInput{input})
    29  	if err != nil {
    30  		return
    31  	}
    32  	output = res[0]
    33  	return
    34  }
    35  
    36  func SignMany(encryptedBase64PrivateKey string, password string, input []SignInput) (output []SignOutput, err error) {
    37  	output = make([]SignOutput, len(input))
    38  
    39  	privateKeyAndSalt, err := base64.StdEncoding.DecodeString(encryptedBase64PrivateKey)
    40  	if err != nil {
    41  		err = fmt.Errorf("zign.Sign: decoding encrypted private key: %w", err)
    42  		return
    43  	}
    44  
    45  	privateKeyAndSaltLen := len(privateKeyAndSalt)
    46  	if privateKeyAndSaltLen < SaltSize+crypto.Ed25519PrivateKeySize {
    47  		err = errors.New("zign.Sign: private key is not valid")
    48  		return
    49  	}
    50  
    51  	encryptedPrivateKey := privateKeyAndSalt[:len(privateKeyAndSalt)-SaltSize]
    52  	salt := privateKeyAndSalt[len(encryptedPrivateKey):]
    53  
    54  	encryptionKey, err := crypto.DeriveKeyFromPassword([]byte(password), salt, crypto.DefaultDeriveKeyFromPasswordParams)
    55  	if err != nil {
    56  		err = fmt.Errorf("zign.Sign: deriving encryption key from password: %w", err)
    57  		return
    58  	}
    59  
    60  	privateKeyBytes, err := crypto.Decrypt(encryptionKey, encryptedPrivateKey, salt)
    61  	if err != nil {
    62  		err = fmt.Errorf("zign.Sign: decrypting private key: %w", err)
    63  		return
    64  	}
    65  	defer crypto.Zeroize(privateKeyBytes)
    66  
    67  	privateKey, err := crypto.NewEd25519PrivateKeyFromBytes(privateKeyBytes)
    68  	if err != nil {
    69  		err = fmt.Errorf("zign.Sign: parsing private key: %w", err)
    70  		return
    71  	}
    72  	defer crypto.Zeroize(privateKey)
    73  
    74  	for index, file := range input {
    75  		var hash []byte
    76  		var signature []byte
    77  
    78  		hash, signature, err = hashAndSignFile(privateKey, file.Reader)
    79  		if err != nil {
    80  			return
    81  		}
    82  
    83  		output[index] = SignOutput{
    84  			Filename:   file.Filename,
    85  			HashBlake3: hex.EncodeToString(hash),
    86  			Signature:  signature,
    87  		}
    88  	}
    89  
    90  	return
    91  }
    92  
    93  func hashAndSignFile(privateKey crypto.Ed25519PrivateKey, file io.Reader) (hash, signature []byte, err error) {
    94  	hasher := blake3.New()
    95  	var size int64
    96  
    97  	size, err = io.Copy(hasher, file)
    98  	if err != nil {
    99  		err = fmt.Errorf("zign.Sign: hashing file %w", err)
   100  		return
   101  	}
   102  
   103  	hash = hasher.Sum(nil)
   104  
   105  	// size of an uint64 and hash
   106  	sizeUint64 := uint64(size)
   107  	message := bytes.NewBuffer(make([]byte, 0, 8+crypto.HashSize256))
   108  	err = binary.Write(message, binary.BigEndian, sizeUint64)
   109  	if err != nil {
   110  		err = fmt.Errorf("zign.Sign: writing size: %w", err)
   111  		return
   112  	}
   113  
   114  	_, err = message.Write(hash)
   115  	if err != nil {
   116  		err = fmt.Errorf("zign.Sign: writing hash: %w", err)
   117  		return
   118  	}
   119  
   120  	signature, err = privateKey.Sign(crypto.RandReader(), message.Bytes(), crypto.Ed25519SignerOpts)
   121  	if err != nil {
   122  		err = fmt.Errorf("zign.Sign: signing file: %w", err)
   123  		return
   124  	}
   125  
   126  	return
   127  }