-
Notifications
You must be signed in to change notification settings - Fork 2
/
verified-sms.go
185 lines (148 loc) · 4.93 KB
/
verified-sms.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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package verifiedsms
import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/base64"
"encoding/json"
"github.com/monzo/terrors"
data_munging "github.com/monzo/verifiedsms/data-munging"
"github.com/monzo/verifiedsms/hashing"
"github.com/monzo/verifiedsms/oauth2"
"net/http"
)
const (
ApiGetPublicKeysUrl = "https://verifiedsms.googleapis.com/v1/enabledUserKeys:batchGet"
ApiSubmitHashesUrl = "https://verifiedsms.googleapis.com/v1/messages:batchCreate"
ContentTypeHeader = "application/json"
UserAgentHeader = "monzo/verifiedsms"
)
type Partner struct {
// The JSON keys for a service account that will make requests to create messages and enable user keys as the
// Verified SMS partner
ServiceAccountJSONFile string
}
type Agent struct {
// The ID of the Verified SMS agent to use
ID string
// The private key of the Verified SMS agent to use
PrivateKey *ecdsa.PrivateKey
}
// MarkSMSAsVerified marks a given SMS as verified for a given end users phone number
// agent is a VerifiedSMSAgent that the message will appear to be sent from
// smsMessage is the content of the message to be verified
// Returns a boolean to indicate whether the SMS was verified, this will be false if there were no errors but the users'
// device just doesn't support Verified SMS
// An error will be returned if we couldn't mark the SMS as Verified and we aren't sure whether the user is on
// Verified SMS
func (partner Partner) MarkSMSAsVerified(ctx context.Context, phoneNumber string, agent *Agent, smsMessage string) (bool, error) {
publicKeys, err := partner.GetPhoneNumberPublicKeys(ctx, phoneNumber)
if err != nil {
return false, terrors.Propagate(err)
}
if len(publicKeys) == 0 {
return false, nil
}
var messagesToGoogle []messageSubmissionToGoogle
smsMessages := data_munging.GetAllIterationsOfSMSMessage(smsMessage)
for _, publicKey := range publicKeys {
for _, smsMessageEntry := range smsMessages {
hash, err := hashing.GetHashForSMSMessage(publicKey, agent.PrivateKey, []byte(smsMessageEntry))
if err != nil {
return false, terrors.Propagate(err)
}
messagesToGoogle = append(messagesToGoogle, messageSubmissionToGoogle{
Hash: base64.StdEncoding.EncodeToString(hash),
AgentId: agent.ID,
})
}
}
requestStruct := batchSubmitRequest{
Messages: messagesToGoogle,
}
requestBody, err := json.Marshal(requestStruct)
if err != nil {
return false, terrors.Propagate(err)
}
request, err := http.NewRequest("POST", ApiSubmitHashesUrl, bytes.NewReader(requestBody))
if err != nil {
return false, terrors.Propagate(err)
}
request.Header.Set("Content-Type", ContentTypeHeader)
request.Header.Set("User-Agent", UserAgentHeader)
client, err := oauth2.GetHttpClient(ctx, partner.ServiceAccountJSONFile)
if err != nil {
return false, terrors.Propagate(err)
}
httpResponse, err := client.Do(request)
if err != nil {
return false, terrors.Propagate(err)
}
if httpResponse.StatusCode < 200 || httpResponse.StatusCode > 299 {
return false, terrors.InternalService(
terrors.ErrInternalService,
"bad response from Google: "+httpResponse.Status,
nil,
)
}
return true, nil
}
// GetPhoneNumberPublicKeys gets the public keys for a given phone number from the Verified SMS service and returns them
// as a slice of strings
func (partner Partner) GetPhoneNumberPublicKeys(ctx context.Context, phoneNumber string) ([]string, error) {
requestBody, err := json.Marshal(map[string][]string{
"phoneNumbers": {
phoneNumber,
},
})
if err != nil {
return nil, terrors.Propagate(err)
}
request, err := http.NewRequest("POST", ApiGetPublicKeysUrl, bytes.NewReader(requestBody))
if err != nil {
return nil, terrors.Propagate(err)
}
request.Header.Set("Content-Type", ContentTypeHeader)
request.Header.Set("User-Agent", UserAgentHeader)
client, err := oauth2.GetHttpClient(ctx, partner.ServiceAccountJSONFile)
if err != nil {
return nil, terrors.Propagate(err)
}
httpResponse, err := client.Do(request)
if err != nil {
return nil, terrors.Propagate(err)
}
if httpResponse.StatusCode < 200 || httpResponse.StatusCode > 299 {
return nil, terrors.InternalService(
terrors.ErrInternalService,
"bad response from Google: "+httpResponse.Status,
nil,
)
}
response := verifiedSMSResponse{}
err = json.NewDecoder(httpResponse.Body).Decode(&response)
if err != nil {
return nil, terrors.Propagate(err)
}
var publicKeys []string
for _, keys := range response.UserKeys {
if keys.PhoneNumber == phoneNumber {
publicKeys = append(publicKeys, keys.PublicKey)
}
}
return publicKeys, nil
}
type verifiedSMSResponse struct {
UserKeys []verifiedSMSResponseUserKeys `json:"userKeys"`
}
type verifiedSMSResponseUserKeys struct {
PhoneNumber string `json:"phoneNumber"`
PublicKey string `json:"publicKey"`
}
type messageSubmissionToGoogle struct {
Hash string `json:"hash"`
AgentId string `json:"agentId"`
}
type batchSubmitRequest struct {
Messages []messageSubmissionToGoogle `json:"messages"`
}