/
rsahelpers.go
162 lines (143 loc) · 4.73 KB
/
rsahelpers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package rsahelpers
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"os"
"os/user"
"path/filepath"
"github.com/ostrowr/send-me-a-secret/internal/utils"
"golang.org/x/crypto/ssh"
)
// Github strips out comments and doesn't allow options on public keys, and I haven't figured out an elegant way
// to mark a public key as belonging to send-me-a-secret without doing something weird like
// creating a gist or updating a bio to point to the right key. In the meantime, we'll use this
// nontraditional key length and assume the user has no other keys of length 4567.
// Encryption will fail if there is not exactly one key of length WeirdKeyLength in the github user's account.
const WeirdKeyLength = 4568
var KeyFilename = ".send-me-a-secret"
func PathToKeyFile() string {
usr, err := user.Current()
if err != nil {
panic(err)
}
return filepath.Join(usr.HomeDir, KeyFilename)
}
// GenerateKey generates a new RSA private key with key length WEIRD_KEY_LENGTH
func GenerateKey() (*rsa.PrivateKey, error) {
rng := rand.Reader
return rsa.GenerateKey(rng, WeirdKeyLength)
}
// WritePrivateKeyToFile writes an rsa private key to ~/.send-me-a-secret
// This path is not configurable; don't want a user to be able to forget
// where they saved their key.
func WritePrivateKeyToFile(password []byte, privateKey *rsa.PrivateKey) error {
keyfile, err := os.Create(PathToKeyFile())
if err != nil {
return err
}
err = os.Chmod(PathToKeyFile(), 0600)
if err != nil {
return err
}
defer utils.MustClose(keyfile)
return writePrivateKey(password, privateKey, keyfile)
}
func writePrivateKey(password []byte, privateKey *rsa.PrivateKey, dest io.Writer) error {
block := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
encryptedBlock, err := x509.EncryptPEMBlock(
rand.Reader,
block.Type,
block.Bytes,
password,
x509.PEMCipherAES256,
)
if err != nil {
return err
}
err = pem.Encode(dest, encryptedBlock)
if err != nil {
return err
}
return nil
}
// ReadPrivateKeyFromFile reads an rsa private key from ~/.send-me-a-secret
// This path is not configurable; don't want a user to be able to forget
// where they saved their key.
func ReadPrivateKeyFromFile(password []byte) (*rsa.PrivateKey, error) {
keyPem, err := os.ReadFile(PathToKeyFile())
if err != nil {
return nil, err
}
return readPrivateKey(password, keyPem)
}
func readPrivateKey(password, keyPem []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(keyPem)
isEncrypted := x509.IsEncryptedPEMBlock(block)
pemBytes := block.Bytes
if isEncrypted {
var err error
pemBytes, err = x509.DecryptPEMBlock(block, password)
if err != nil {
return nil, err
}
}
return x509.ParsePKCS1PrivateKey(pemBytes)
}
// GetSSHPublicKey generates a public key suitable for openssh (and thus GitHub) from a private key.
func GetSSHPublicKey(privateKey *rsa.PrivateKey) ([]byte, error) {
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, err
}
marshaled := ssh.MarshalAuthorizedKey(publicKey)
return marshaled, nil
}
var ErrInvalidPublicKey = errors.New("invalid public key")
func SSHPubKeyToRSAPubKey(sshPubKey []byte) (*rsa.PublicKey, error) {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(sshPubKey)
if err != nil {
return nil, err
}
keyAsCryptoKey, ok := pubKey.(ssh.CryptoPublicKey)
if !ok {
return nil, ErrInvalidPublicKey
}
rsaPubKey, ok := keyAsCryptoKey.CryptoPublicKey().(*rsa.PublicKey)
if !ok {
return nil, ErrInvalidPublicKey
}
return rsaPubKey, nil
}
// Encrypt encrypts a message under the given public key, suitable for decrypting via `Decrypt`
func Encrypt(publicKey *rsa.PublicKey, message []byte) (string, error) {
rng := rand.Reader
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rng, publicKey, message, nil)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts a message using the given private key which was encrypted by `Encrypt`
func Decrypt(privateKey *rsa.PrivateKey, base64EncodedCiphertext string) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(base64EncodedCiphertext)
if err != nil {
return nil, err
}
rng := rand.Reader
return rsa.DecryptOAEP(sha256.New(), rng, privateKey, ciphertext, nil)
}
// IsValidSendMeASecretKey checks if the key fetched from GitHub is the key uploaded by send-me-a-secret.
// Right now, this just checks that length of the key is WEIRD_KEY_LENGTH, hoping that the user doesn't have
// any other keys of that length, but hopefully in the future we'll be able to do something a bit cleverer.
func IsValidSendMeASecretKey(publicKey *rsa.PublicKey) bool {
return publicKey.Size() == WeirdKeyLength/8
}