main
1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package openpgp
6
7import (
8 "crypto"
9 "hash"
10 "io"
11 "strconv"
12 "time"
13
14 "github.com/ProtonMail/go-crypto/openpgp/armor"
15 "github.com/ProtonMail/go-crypto/openpgp/errors"
16 "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
17 "github.com/ProtonMail/go-crypto/openpgp/packet"
18)
19
20// DetachSign signs message with the private key from signer (which must
21// already have been decrypted) and writes the signature to w.
22// If config is nil, sensible defaults will be used.
23func DetachSign(w io.Writer, signer *Entity, message io.Reader, config *packet.Config) error {
24 return detachSign(w, signer, message, packet.SigTypeBinary, config)
25}
26
27// ArmoredDetachSign signs message with the private key from signer (which
28// must already have been decrypted) and writes an armored signature to w.
29// If config is nil, sensible defaults will be used.
30func ArmoredDetachSign(w io.Writer, signer *Entity, message io.Reader, config *packet.Config) (err error) {
31 return armoredDetachSign(w, signer, message, packet.SigTypeBinary, config)
32}
33
34// DetachSignText signs message (after canonicalising the line endings) with
35// the private key from signer (which must already have been decrypted) and
36// writes the signature to w.
37// If config is nil, sensible defaults will be used.
38func DetachSignText(w io.Writer, signer *Entity, message io.Reader, config *packet.Config) error {
39 return detachSign(w, signer, message, packet.SigTypeText, config)
40}
41
42// ArmoredDetachSignText signs message (after canonicalising the line endings)
43// with the private key from signer (which must already have been decrypted)
44// and writes an armored signature to w.
45// If config is nil, sensible defaults will be used.
46func ArmoredDetachSignText(w io.Writer, signer *Entity, message io.Reader, config *packet.Config) error {
47 return armoredDetachSign(w, signer, message, packet.SigTypeText, config)
48}
49
50func armoredDetachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.SignatureType, config *packet.Config) (err error) {
51 out, err := armor.Encode(w, SignatureType, nil)
52 if err != nil {
53 return
54 }
55 err = detachSign(out, signer, message, sigType, config)
56 if err != nil {
57 return
58 }
59 return out.Close()
60}
61
62func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.SignatureType, config *packet.Config) (err error) {
63 signingKey, ok := signer.SigningKeyById(config.Now(), config.SigningKey())
64 if !ok {
65 return errors.InvalidArgumentError("no valid signing keys")
66 }
67 if signingKey.PrivateKey == nil {
68 return errors.InvalidArgumentError("signing key doesn't have a private key")
69 }
70 if signingKey.PrivateKey.Encrypted {
71 return errors.InvalidArgumentError("signing key is encrypted")
72 }
73 if _, ok := algorithm.HashToHashId(config.Hash()); !ok {
74 return errors.InvalidArgumentError("invalid hash function")
75 }
76
77 sig := createSignaturePacket(signingKey.PublicKey, sigType, config)
78
79 h, err := sig.PrepareSign(config)
80 if err != nil {
81 return
82 }
83 wrappedHash, err := wrapHashForSignature(h, sig.SigType)
84 if err != nil {
85 return
86 }
87 if _, err = io.Copy(wrappedHash, message); err != nil {
88 return err
89 }
90
91 err = sig.Sign(h, signingKey.PrivateKey, config)
92 if err != nil {
93 return
94 }
95
96 return sig.Serialize(w)
97}
98
99// FileHints contains metadata about encrypted files. This metadata is, itself,
100// encrypted.
101type FileHints struct {
102 // IsBinary can be set to hint that the contents are binary data.
103 IsBinary bool
104 // FileName hints at the name of the file that should be written. It's
105 // truncated to 255 bytes if longer. It may be empty to suggest that the
106 // file should not be written to disk. It may be equal to "_CONSOLE" to
107 // suggest the data should not be written to disk.
108 FileName string
109 // ModTime contains the modification time of the file, or the zero time if not applicable.
110 ModTime time.Time
111}
112
113// SymmetricallyEncrypt acts like gpg -c: it encrypts a file with a passphrase.
114// The resulting WriteCloser must be closed after the contents of the file have
115// been written.
116// If config is nil, sensible defaults will be used.
117func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
118 if hints == nil {
119 hints = &FileHints{}
120 }
121
122 key, err := packet.SerializeSymmetricKeyEncrypted(ciphertext, passphrase, config)
123 if err != nil {
124 return
125 }
126
127 var w io.WriteCloser
128 cipherSuite := packet.CipherSuite{
129 Cipher: config.Cipher(),
130 Mode: config.AEAD().Mode(),
131 }
132 w, err = packet.SerializeSymmetricallyEncrypted(ciphertext, config.Cipher(), config.AEAD() != nil, cipherSuite, key, config)
133 if err != nil {
134 return
135 }
136
137 literalData := w
138 if algo := config.Compression(); algo != packet.CompressionNone {
139 var compConfig *packet.CompressionConfig
140 if config != nil {
141 compConfig = config.CompressionConfig
142 }
143 literalData, err = packet.SerializeCompressed(w, algo, compConfig)
144 if err != nil {
145 return
146 }
147 }
148
149 var epochSeconds uint32
150 if !hints.ModTime.IsZero() {
151 epochSeconds = uint32(hints.ModTime.Unix())
152 }
153 return packet.SerializeLiteral(literalData, hints.IsBinary, hints.FileName, epochSeconds)
154}
155
156// intersectPreferences mutates and returns a prefix of a that contains only
157// the values in the intersection of a and b. The order of a is preserved.
158func intersectPreferences(a []uint8, b []uint8) (intersection []uint8) {
159 var j int
160 for _, v := range a {
161 for _, v2 := range b {
162 if v == v2 {
163 a[j] = v
164 j++
165 break
166 }
167 }
168 }
169
170 return a[:j]
171}
172
173// intersectPreferences mutates and returns a prefix of a that contains only
174// the values in the intersection of a and b. The order of a is preserved.
175func intersectCipherSuites(a [][2]uint8, b [][2]uint8) (intersection [][2]uint8) {
176 var j int
177 for _, v := range a {
178 for _, v2 := range b {
179 if v[0] == v2[0] && v[1] == v2[1] {
180 a[j] = v
181 j++
182 break
183 }
184 }
185 }
186
187 return a[:j]
188}
189
190func hashToHashId(h crypto.Hash) uint8 {
191 v, ok := algorithm.HashToHashId(h)
192 if !ok {
193 panic("tried to convert unknown hash")
194 }
195 return v
196}
197
198// EncryptText encrypts a message to a number of recipients and, optionally,
199// signs it. Optional information is contained in 'hints', also encrypted, that
200// aids the recipients in processing the message. The resulting WriteCloser
201// must be closed after the contents of the file have been written. If config
202// is nil, sensible defaults will be used. The signing is done in text mode.
203func EncryptText(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
204 return encrypt(ciphertext, ciphertext, to, signed, hints, packet.SigTypeText, config)
205}
206
207// Encrypt encrypts a message to a number of recipients and, optionally, signs
208// it. hints contains optional information, that is also encrypted, that aids
209// the recipients in processing the message. The resulting WriteCloser must
210// be closed after the contents of the file have been written.
211// If config is nil, sensible defaults will be used.
212func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
213 return encrypt(ciphertext, ciphertext, to, signed, hints, packet.SigTypeBinary, config)
214}
215
216// EncryptSplit encrypts a message to a number of recipients and, optionally, signs
217// it. hints contains optional information, that is also encrypted, that aids
218// the recipients in processing the message. The resulting WriteCloser must
219// be closed after the contents of the file have been written.
220// If config is nil, sensible defaults will be used.
221func EncryptSplit(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
222 return encrypt(keyWriter, dataWriter, to, signed, hints, packet.SigTypeBinary, config)
223}
224
225// EncryptTextSplit encrypts a message to a number of recipients and, optionally, signs
226// it. hints contains optional information, that is also encrypted, that aids
227// the recipients in processing the message. The resulting WriteCloser must
228// be closed after the contents of the file have been written.
229// If config is nil, sensible defaults will be used.
230func EncryptTextSplit(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
231 return encrypt(keyWriter, dataWriter, to, signed, hints, packet.SigTypeText, config)
232}
233
234// writeAndSign writes the data as a payload package and, optionally, signs
235// it. hints contains optional information, that is also encrypted,
236// that aids the recipients in processing the message. The resulting
237// WriteCloser must be closed after the contents of the file have been
238// written. If config is nil, sensible defaults will be used.
239func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entity, hints *FileHints, sigType packet.SignatureType, config *packet.Config) (plaintext io.WriteCloser, err error) {
240 var signer *packet.PrivateKey
241 if signed != nil {
242 signKey, ok := signed.SigningKeyById(config.Now(), config.SigningKey())
243 if !ok {
244 return nil, errors.InvalidArgumentError("no valid signing keys")
245 }
246 signer = signKey.PrivateKey
247 if signer == nil {
248 return nil, errors.InvalidArgumentError("no private key in signing key")
249 }
250 if signer.Encrypted {
251 return nil, errors.InvalidArgumentError("signing key must be decrypted")
252 }
253 }
254
255 var hash crypto.Hash
256 for _, hashId := range candidateHashes {
257 if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() {
258 hash = h
259 break
260 }
261 }
262
263 // If the hash specified by config is a candidate, we'll use that.
264 if configuredHash := config.Hash(); configuredHash.Available() {
265 for _, hashId := range candidateHashes {
266 if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash {
267 hash = h
268 break
269 }
270 }
271 }
272
273 if hash == 0 {
274 hashId := candidateHashes[0]
275 name, ok := algorithm.HashIdToString(hashId)
276 if !ok {
277 name = "#" + strconv.Itoa(int(hashId))
278 }
279 return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
280 }
281
282 var salt []byte
283 if signer != nil {
284 var opsVersion = 3
285 if signer.Version == 6 {
286 opsVersion = signer.Version
287 }
288 ops := &packet.OnePassSignature{
289 Version: opsVersion,
290 SigType: sigType,
291 Hash: hash,
292 PubKeyAlgo: signer.PubKeyAlgo,
293 KeyId: signer.KeyId,
294 IsLast: true,
295 }
296 if opsVersion == 6 {
297 ops.KeyFingerprint = signer.Fingerprint
298 salt, err = packet.SignatureSaltForHash(hash, config.Random())
299 if err != nil {
300 return nil, err
301 }
302 ops.Salt = salt
303 }
304 if err := ops.Serialize(payload); err != nil {
305 return nil, err
306 }
307 }
308
309 if hints == nil {
310 hints = &FileHints{}
311 }
312
313 w := payload
314 if signer != nil {
315 // If we need to write a signature packet after the literal
316 // data then we need to stop literalData from closing
317 // encryptedData.
318 w = noOpCloser{w}
319
320 }
321 var epochSeconds uint32
322 if !hints.ModTime.IsZero() {
323 epochSeconds = uint32(hints.ModTime.Unix())
324 }
325 literalData, err := packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds)
326 if err != nil {
327 return nil, err
328 }
329
330 if signer != nil {
331 h, wrappedHash, err := hashForSignature(hash, sigType, salt)
332 if err != nil {
333 return nil, err
334 }
335 metadata := &packet.LiteralData{
336 Format: 'u',
337 FileName: hints.FileName,
338 Time: epochSeconds,
339 }
340 if hints.IsBinary {
341 metadata.Format = 'b'
342 }
343 return signatureWriter{payload, literalData, hash, wrappedHash, h, salt, signer, sigType, config, metadata}, nil
344 }
345 return literalData, nil
346}
347
348// encrypt encrypts a message to a number of recipients and, optionally, signs
349// it. hints contains optional information, that is also encrypted, that aids
350// the recipients in processing the message. The resulting WriteCloser must
351// be closed after the contents of the file have been written.
352// If config is nil, sensible defaults will be used.
353func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *Entity, hints *FileHints, sigType packet.SignatureType, config *packet.Config) (plaintext io.WriteCloser, err error) {
354 if len(to) == 0 {
355 return nil, errors.InvalidArgumentError("no encryption recipient provided")
356 }
357
358 // These are the possible ciphers that we'll use for the message.
359 candidateCiphers := []uint8{
360 uint8(packet.CipherAES256),
361 uint8(packet.CipherAES128),
362 }
363
364 // These are the possible hash functions that we'll use for the signature.
365 candidateHashes := []uint8{
366 hashToHashId(crypto.SHA256),
367 hashToHashId(crypto.SHA384),
368 hashToHashId(crypto.SHA512),
369 hashToHashId(crypto.SHA3_256),
370 hashToHashId(crypto.SHA3_512),
371 }
372
373 // Prefer GCM if everyone supports it
374 candidateCipherSuites := [][2]uint8{
375 {uint8(packet.CipherAES256), uint8(packet.AEADModeGCM)},
376 {uint8(packet.CipherAES256), uint8(packet.AEADModeEAX)},
377 {uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)},
378 {uint8(packet.CipherAES128), uint8(packet.AEADModeGCM)},
379 {uint8(packet.CipherAES128), uint8(packet.AEADModeEAX)},
380 {uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)},
381 }
382
383 candidateCompression := []uint8{
384 uint8(packet.CompressionNone),
385 uint8(packet.CompressionZIP),
386 uint8(packet.CompressionZLIB),
387 }
388
389 encryptKeys := make([]Key, len(to))
390
391 // AEAD is used only if config enables it and every key supports it
392 aeadSupported := config.AEAD() != nil
393
394 for i := range to {
395 var ok bool
396 encryptKeys[i], ok = to[i].EncryptionKey(config.Now())
397 if !ok {
398 return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys")
399 }
400
401 primarySelfSignature, _ := to[i].PrimarySelfSignature()
402 if primarySelfSignature == nil {
403 return nil, errors.InvalidArgumentError("entity without a self-signature")
404 }
405
406 if !primarySelfSignature.SEIPDv2 {
407 aeadSupported = false
408 }
409
410 candidateCiphers = intersectPreferences(candidateCiphers, primarySelfSignature.PreferredSymmetric)
411 candidateHashes = intersectPreferences(candidateHashes, primarySelfSignature.PreferredHash)
412 candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, primarySelfSignature.PreferredCipherSuites)
413 candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression)
414 }
415
416 // In the event that the intersection of supported algorithms is empty we use the ones
417 // labelled as MUST that every implementation supports.
418 if len(candidateCiphers) == 0 {
419 // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.3
420 candidateCiphers = []uint8{uint8(packet.CipherAES128)}
421 }
422 if len(candidateHashes) == 0 {
423 // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#hash-algos
424 candidateHashes = []uint8{hashToHashId(crypto.SHA256)}
425 }
426 if len(candidateCipherSuites) == 0 {
427 // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6
428 candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}}
429 }
430
431 cipher := packet.CipherFunction(candidateCiphers[0])
432 aeadCipherSuite := packet.CipherSuite{
433 Cipher: packet.CipherFunction(candidateCipherSuites[0][0]),
434 Mode: packet.AEADMode(candidateCipherSuites[0][1]),
435 }
436
437 // If the cipher specified by config is a candidate, we'll use that.
438 configuredCipher := config.Cipher()
439 for _, c := range candidateCiphers {
440 cipherFunc := packet.CipherFunction(c)
441 if cipherFunc == configuredCipher {
442 cipher = cipherFunc
443 break
444 }
445 }
446
447 var symKey []byte
448 if aeadSupported {
449 symKey = make([]byte, aeadCipherSuite.Cipher.KeySize())
450 } else {
451 symKey = make([]byte, cipher.KeySize())
452 }
453
454 if _, err := io.ReadFull(config.Random(), symKey); err != nil {
455 return nil, err
456 }
457
458 for _, key := range encryptKeys {
459 if err := packet.SerializeEncryptedKeyAEAD(keyWriter, key.PublicKey, cipher, aeadSupported, symKey, config); err != nil {
460 return nil, err
461 }
462 }
463
464 var payload io.WriteCloser
465 payload, err = packet.SerializeSymmetricallyEncrypted(dataWriter, cipher, aeadSupported, aeadCipherSuite, symKey, config)
466 if err != nil {
467 return
468 }
469
470 payload, err = handleCompression(payload, candidateCompression, config)
471 if err != nil {
472 return nil, err
473 }
474
475 return writeAndSign(payload, candidateHashes, signed, hints, sigType, config)
476}
477
478// Sign signs a message. The resulting WriteCloser must be closed after the
479// contents of the file have been written. hints contains optional information
480// that aids the recipients in processing the message.
481// If config is nil, sensible defaults will be used.
482func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Config) (input io.WriteCloser, err error) {
483 if signed == nil {
484 return nil, errors.InvalidArgumentError("no signer provided")
485 }
486
487 // These are the possible hash functions that we'll use for the signature.
488 candidateHashes := []uint8{
489 hashToHashId(crypto.SHA256),
490 hashToHashId(crypto.SHA384),
491 hashToHashId(crypto.SHA512),
492 hashToHashId(crypto.SHA3_256),
493 hashToHashId(crypto.SHA3_512),
494 }
495 defaultHashes := candidateHashes[0:1]
496 primarySelfSignature, _ := signed.PrimarySelfSignature()
497 if primarySelfSignature == nil {
498 return nil, errors.StructuralError("signed entity has no self-signature")
499 }
500 preferredHashes := primarySelfSignature.PreferredHash
501 if len(preferredHashes) == 0 {
502 preferredHashes = defaultHashes
503 }
504 candidateHashes = intersectPreferences(candidateHashes, preferredHashes)
505 if len(candidateHashes) == 0 {
506 return nil, errors.StructuralError("cannot sign because signing key shares no common algorithms with candidate hashes")
507 }
508
509 return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, packet.SigTypeBinary, config)
510}
511
512// signatureWriter hashes the contents of a message while passing it along to
513// literalData. When closed, it closes literalData, writes a signature packet
514// to encryptedData and then also closes encryptedData.
515type signatureWriter struct {
516 encryptedData io.WriteCloser
517 literalData io.WriteCloser
518 hashType crypto.Hash
519 wrappedHash hash.Hash
520 h hash.Hash
521 salt []byte // v6 only
522 signer *packet.PrivateKey
523 sigType packet.SignatureType
524 config *packet.Config
525 metadata *packet.LiteralData // V5 signatures protect document metadata
526}
527
528func (s signatureWriter) Write(data []byte) (int, error) {
529 s.wrappedHash.Write(data)
530 switch s.sigType {
531 case packet.SigTypeBinary:
532 return s.literalData.Write(data)
533 case packet.SigTypeText:
534 flag := 0
535 return writeCanonical(s.literalData, data, &flag)
536 }
537 return 0, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(s.sigType)))
538}
539
540func (s signatureWriter) Close() error {
541 sig := createSignaturePacket(&s.signer.PublicKey, s.sigType, s.config)
542 sig.Hash = s.hashType
543 sig.Metadata = s.metadata
544
545 if err := sig.SetSalt(s.salt); err != nil {
546 return err
547 }
548
549 if err := sig.Sign(s.h, s.signer, s.config); err != nil {
550 return err
551 }
552 if err := s.literalData.Close(); err != nil {
553 return err
554 }
555 if err := sig.Serialize(s.encryptedData); err != nil {
556 return err
557 }
558 return s.encryptedData.Close()
559}
560
561func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature {
562 sigLifetimeSecs := config.SigLifetime()
563 return &packet.Signature{
564 Version: signer.Version,
565 SigType: sigType,
566 PubKeyAlgo: signer.PubKeyAlgo,
567 Hash: config.Hash(),
568 CreationTime: config.Now(),
569 IssuerKeyId: &signer.KeyId,
570 IssuerFingerprint: signer.Fingerprint,
571 Notations: config.Notations(),
572 SigLifetimeSecs: &sigLifetimeSecs,
573 }
574}
575
576// noOpCloser is like an ioutil.NopCloser, but for an io.Writer.
577// TODO: we have two of these in OpenPGP packages alone. This probably needs
578// to be promoted somewhere more common.
579type noOpCloser struct {
580 w io.Writer
581}
582
583func (c noOpCloser) Write(data []byte) (n int, err error) {
584 return c.w.Write(data)
585}
586
587func (c noOpCloser) Close() error {
588 return nil
589}
590
591func handleCompression(compressed io.WriteCloser, candidateCompression []uint8, config *packet.Config) (data io.WriteCloser, err error) {
592 data = compressed
593 confAlgo := config.Compression()
594 if confAlgo == packet.CompressionNone {
595 return
596 }
597
598 // Set algorithm labelled as MUST as fallback
599 // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.4
600 finalAlgo := packet.CompressionNone
601 // if compression specified by config available we will use it
602 for _, c := range candidateCompression {
603 if uint8(confAlgo) == c {
604 finalAlgo = confAlgo
605 break
606 }
607 }
608
609 if finalAlgo != packet.CompressionNone {
610 var compConfig *packet.CompressionConfig
611 if config != nil {
612 compConfig = config.CompressionConfig
613 }
614 data, err = packet.SerializeCompressed(compressed, finalAlgo, compConfig)
615 if err != nil {
616 return
617 }
618 }
619 return data, nil
620}