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 }