1/*
2Copyright 2017 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package envelope transforms values for storage at rest using a Envelope provider
18package envelope
19
20import (
21	"context"
22	"fmt"
23	"net"
24	"net/url"
25	"strings"
26	"sync"
27	"time"
28
29	"k8s.io/klog/v2"
30
31	"google.golang.org/grpc"
32
33	kmsapi "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/v1beta1"
34)
35
36const (
37	// Now only supported unix domain socket.
38	unixProtocol = "unix"
39
40	// Current version for the protocol interface definition.
41	kmsapiVersion = "v1beta1"
42
43	versionErrorf = "KMS provider api version %s is not supported, only %s is supported now"
44)
45
46// The gRPC implementation for envelope.Service.
47type gRPCService struct {
48	kmsClient      kmsapi.KeyManagementServiceClient
49	connection     *grpc.ClientConn
50	callTimeout    time.Duration
51	mux            sync.RWMutex
52	versionChecked bool
53}
54
55// NewGRPCService returns an envelope.Service which use gRPC to communicate the remote KMS provider.
56func NewGRPCService(endpoint string, callTimeout time.Duration) (Service, error) {
57	klog.V(4).Infof("Configure KMS provider with endpoint: %s", endpoint)
58
59	addr, err := parseEndpoint(endpoint)
60	if err != nil {
61		return nil, err
62	}
63
64	s := &gRPCService{callTimeout: callTimeout}
65	s.connection, err = grpc.Dial(
66		addr,
67		grpc.WithInsecure(),
68		grpc.WithUnaryInterceptor(s.interceptor),
69		grpc.WithDefaultCallOptions(grpc.WaitForReady(true)),
70		grpc.WithContextDialer(
71			func(context.Context, string) (net.Conn, error) {
72				// Ignoring addr and timeout arguments:
73				// addr - comes from the closure
74				c, err := net.DialUnix(unixProtocol, nil, &net.UnixAddr{Name: addr})
75				if err != nil {
76					klog.Errorf("failed to create connection to unix socket: %s, error: %v", addr, err)
77				} else {
78					klog.V(4).Infof("Successfully dialed Unix socket %v", addr)
79				}
80				return c, err
81			}))
82
83	if err != nil {
84		return nil, fmt.Errorf("failed to create connection to %s, error: %v", endpoint, err)
85	}
86
87	s.kmsClient = kmsapi.NewKeyManagementServiceClient(s.connection)
88	return s, nil
89}
90
91// Parse the endpoint to extract schema, host or path.
92func parseEndpoint(endpoint string) (string, error) {
93	if len(endpoint) == 0 {
94		return "", fmt.Errorf("remote KMS provider can't use empty string as endpoint")
95	}
96
97	u, err := url.Parse(endpoint)
98	if err != nil {
99		return "", fmt.Errorf("invalid endpoint %q for remote KMS provider, error: %v", endpoint, err)
100	}
101
102	if u.Scheme != unixProtocol {
103		return "", fmt.Errorf("unsupported scheme %q for remote KMS provider", u.Scheme)
104	}
105
106	// Linux abstract namespace socket - no physical file required
107	// Warning: Linux Abstract sockets have not concept of ACL (unlike traditional file based sockets).
108	// However, Linux Abstract sockets are subject to Linux networking namespace, so will only be accessible to
109	// containers within the same pod (unless host networking is used).
110	if strings.HasPrefix(u.Path, "/@") {
111		return strings.TrimPrefix(u.Path, "/"), nil
112	}
113
114	return u.Path, nil
115}
116
117func (g *gRPCService) checkAPIVersion(ctx context.Context) error {
118	g.mux.Lock()
119	defer g.mux.Unlock()
120
121	if g.versionChecked {
122		return nil
123	}
124
125	request := &kmsapi.VersionRequest{Version: kmsapiVersion}
126	response, err := g.kmsClient.Version(ctx, request)
127	if err != nil {
128		return fmt.Errorf("failed get version from remote KMS provider: %v", err)
129	}
130	if response.Version != kmsapiVersion {
131		return fmt.Errorf(versionErrorf, response.Version, kmsapiVersion)
132	}
133	g.versionChecked = true
134
135	klog.V(4).Infof("Version of KMS provider is %s", response.Version)
136	return nil
137}
138
139// Decrypt a given data string to obtain the original byte data.
140func (g *gRPCService) Decrypt(cipher []byte) ([]byte, error) {
141	ctx, cancel := context.WithTimeout(context.Background(), g.callTimeout)
142	defer cancel()
143
144	request := &kmsapi.DecryptRequest{Cipher: cipher, Version: kmsapiVersion}
145	response, err := g.kmsClient.Decrypt(ctx, request)
146	if err != nil {
147		return nil, err
148	}
149	return response.Plain, nil
150}
151
152// Encrypt bytes to a string ciphertext.
153func (g *gRPCService) Encrypt(plain []byte) ([]byte, error) {
154	ctx, cancel := context.WithTimeout(context.Background(), g.callTimeout)
155	defer cancel()
156
157	request := &kmsapi.EncryptRequest{Plain: plain, Version: kmsapiVersion}
158	response, err := g.kmsClient.Encrypt(ctx, request)
159	if err != nil {
160		return nil, err
161	}
162	return response.Cipher, nil
163}
164
165func (g *gRPCService) interceptor(
166	ctx context.Context,
167	method string,
168	req interface{},
169	reply interface{},
170	cc *grpc.ClientConn,
171	invoker grpc.UnaryInvoker,
172	opts ...grpc.CallOption,
173) error {
174	if !kmsapi.IsVersionCheckMethod(method) {
175		if err := g.checkAPIVersion(ctx); err != nil {
176			return err
177		}
178	}
179
180	return invoker(ctx, method, req, reply, cc, opts...)
181}
182