crypto.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. package crypto
  2. import (
  3. "crypto/aes"
  4. "crypto/cipher"
  5. "crypto/rand"
  6. "encoding/base64"
  7. "errors"
  8. "io"
  9. )
  10. const (
  11. versionByte = 0x31 // "1"
  12. gcmTagSize = 16
  13. gcmNonceSize = 12
  14. )
  15. var (
  16. errCiphertextTooShort = errors.New("ciphertext too short")
  17. errCiphertextUnexpectedVersion = errors.New("unsupported ciphertext version")
  18. )
  19. // Encrypt encrypts the given plaintext with the given key using AES-GCM,
  20. // and encodes the (version, tag, nonce, ciphertext) set as base64.
  21. //
  22. // The output format is (|| means concatenate):
  23. // "1" || tag (128 bits) || IV/nonce (96 bits) || ciphertext (remaining)
  24. //
  25. // This format is compatible with Pushbullet's encryption format.
  26. // See https://docs.pushbullet.com/#encryption for details.
  27. func Encrypt(plaintext string, key []byte) (string, error) {
  28. nonce := make([]byte, gcmNonceSize) // Never use more than 2^32 random nonces
  29. if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
  30. return "", err
  31. }
  32. return encryptWithNonce(plaintext, nonce, key)
  33. }
  34. func encryptWithNonce(plaintext string, nonce, key []byte) (string, error) {
  35. block, err := aes.NewCipher(key)
  36. if err != nil {
  37. return "", err
  38. }
  39. aesgcm, err := cipher.NewGCM(block)
  40. if err != nil {
  41. return "", err
  42. }
  43. ciphertextWithTag := aesgcm.Seal(nil, nonce, []byte(plaintext), nil)
  44. tagIndex := len(ciphertextWithTag) - gcmTagSize
  45. ciphertext, tag := ciphertextWithTag[:tagIndex], ciphertextWithTag[tagIndex:]
  46. output := appendSlices([]byte{versionByte}, tag, nonce, ciphertext)
  47. return base64.StdEncoding.EncodeToString(output), nil
  48. }
  49. // Decrypt decodes and decrypts a message that was encrypted with the Encrypt function.
  50. func Decrypt(input string, key []byte) (string, error) {
  51. inputBytes, err := base64.StdEncoding.DecodeString(input)
  52. if err != nil {
  53. return "", err
  54. }
  55. if len(inputBytes) < 1+gcmTagSize+gcmNonceSize {
  56. return "", errCiphertextTooShort
  57. }
  58. version, tag, nonce, ciphertext := inputBytes[0], inputBytes[1:gcmTagSize+1], inputBytes[1+gcmTagSize:1+gcmTagSize+gcmNonceSize], inputBytes[1+gcmTagSize+gcmNonceSize:]
  59. if version != versionByte {
  60. return "", errCiphertextUnexpectedVersion
  61. }
  62. cipherTextWithTag := append(ciphertext, tag...)
  63. block, err := aes.NewCipher(key)
  64. if err != nil {
  65. return "", err
  66. }
  67. aesgcm, err := cipher.NewGCM(block)
  68. if err != nil {
  69. return "", err
  70. }
  71. plaintext, err := aesgcm.Open(nil, nonce, cipherTextWithTag, nil)
  72. if err != nil {
  73. return "", err
  74. }
  75. return string(plaintext), nil
  76. }
  77. func appendSlices(s ...[]byte) []byte {
  78. var output []byte
  79. for _, r := range s {
  80. output = append(output, r...)
  81. }
  82. return output
  83. }