From 08f3d0d6fe68165e4982efd7b5a6abdf65de16ca Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Wed, 8 Jan 2025 16:31:42 +0530 Subject: [PATCH 01/11] Add aws keyspace support --- config/sample_webconfig.conf | 11 +++ db/cassandra/aws_keyspace.go | 144 +++++++++++++++++++++++++++++++ db/cassandra/cassandra_client.go | 15 ++++ go.mod | 6 +- go.sum | 24 ++++++ 5 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 db/cassandra/aws_keyspace.go diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index 02def24..3126a15 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -167,6 +167,17 @@ webconfig { user = "dbuser" test_keyspace = "test_webconfig" is_ssl_enabled = true + port = 9042 + + //Config to create database client to AWS Keyspace using IAM temporary credentials + aws_keyspace_enabled = false + role_based_access_enabled = false + aws_region = "" + aws_keyspace_ca_path = "path_to_file/sf-class2-root.crt" + + //If role_based_access_enabled is true, access_key_id and secret_access_key will be fetched using IAM temporary credentials + access_key_id = "" + secret_access_key = "" } yugabyte { diff --git a/db/cassandra/aws_keyspace.go b/db/cassandra/aws_keyspace.go new file mode 100644 index 0000000..320264d --- /dev/null +++ b/db/cassandra/aws_keyspace.go @@ -0,0 +1,144 @@ +package cassandra + +import ( + "fmt" + "os" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin/sigv4" + "github.com/go-akka/configuration" + "github.com/gocql/gocql" + "github.com/rdkcentral/webconfig/common" + "github.com/rdkcentral/webconfig/security" +) + +func awsKeyspaceClient(conf *configuration.Config, testOnly bool) (*CassandraClient, error) { + var codec *security.AesCodec + var err error + + // build codec + if testOnly { + codec = security.NewTestCodec(conf) + } else { + codec, err = security.NewAesCodec(conf) + if err != nil { + return nil, common.NewError(err) + } + } + + dbconf := conf.GetConfig("webconfig.database.cassandra") + + // init + hosts := dbconf.GetStringList("hosts") + cluster := gocql.NewCluster(hosts...) + + cluster.Consistency = gocql.LocalQuorum + cluster.ProtoVersion = ProtocolVersion + cluster.DisableInitialHostLookup = DisableInitialHostLookup + cluster.Timeout = time.Duration(dbconf.GetInt32("timeout_in_sec", 1)) * time.Second + cluster.ConnectTimeout = time.Duration(dbconf.GetInt32("connect_timeout_in_sec", 1)) * time.Second + cluster.NumConns = int(dbconf.GetInt32("connections", DefaultConnections)) + cluster.Port = int(dbconf.GetInt64("port", DefaultPort)) + + cluster.RetryPolicy = &gocql.DowngradingConsistencyRetryPolicy{ + ConsistencyLevelsToTry: []gocql.Consistency{ + gocql.LocalQuorum, + gocql.LocalOne, + gocql.One, + }, + } + + localDc := dbconf.GetString("local_dc") + if len(localDc) > 0 { + cluster.PoolConfig.HostSelectionPolicy = gocql.DCAwareRoundRobinPolicy(localDc) + } + + awsRegion, err := getAwsRegionForCassandra(dbconf) + if err != nil { + return nil, err + } + + var auth sigv4.AwsAuthenticator = sigv4.NewAwsAuthenticator() + auth.Region = awsRegion + + isRoleBasedAccessEnabled := dbconf.GetBoolean("role_based_access_enabled") + if isRoleBasedAccessEnabled { + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(awsRegion)}, + ) + if err != nil { + return nil, err + } + + // Set up the callback to refresh credentials + auth.CredentialsCallback = func() (sigv4.SigV4Credentials, error) { + creds, err := sess.Config.Credentials.Get() + if err != nil { + return sigv4.SigV4Credentials{}, err + } + + return sigv4.SigV4Credentials{ + AccessKeyId: creds.AccessKeyID, + SecretAccessKey: creds.SecretAccessKey, + SessionToken: creds.SessionToken, + }, nil + } + } else { + auth.AccessKeyId = dbconf.GetString("access_key_id") + auth.SecretAccessKey = dbconf.GetString("secret_access_key") + } + cluster.Authenticator = auth + + awsKeySpaceCaPath := dbconf.GetString("aws_keyspace_ca_path") + cluster.SslOpts = &gocql.SslOptions{ + CaPath: awsKeySpaceCaPath, + EnableHostVerification: false, + } + + // check and create test_keyspace + if testOnly { + cluster.Keyspace = dbconf.GetString("test_keyspace", DefaultTestKeyspace) + } else { + cluster.Keyspace = dbconf.GetString("keyspace", DefaultKeyspace) + } + + // now point to the real keyspace + session, err := cluster.CreateSession() + if err != nil { + return nil, common.NewError(err) + } + session.SetPageSize(int(dbconf.GetInt32("page_size", DefaultPageSize))) + + blockedSubdocIds := conf.GetStringList("webconfig.blocked_subdoc_ids") + encryptedSubdocIds := conf.GetStringList("webconfig.encrypted_subdoc_ids") + stateCorrectionEnabled := conf.GetBoolean("webconfig.state_correction_enabled") + lockRootDocumentEnabled := conf.GetBoolean("webconfig.lock_root_document_enabled") + + return &CassandraClient{ + Session: session, + ClusterConfig: cluster, + AesCodec: codec, + concurrentQueries: make(chan bool, dbconf.GetInt32("concurrent_queries", 500)), + localDc: localDc, + blockedSubdocIds: blockedSubdocIds, + encryptedSubdocIds: encryptedSubdocIds, + stateCorrectionEnabled: stateCorrectionEnabled, + lockRootDocumentEnabled: lockRootDocumentEnabled, + }, nil +} + +func getAwsRegionForCassandra(dbconf *configuration.Config) (string, error) { + awsRegion := dbconf.GetString("aws_region") + + if len(awsRegion) == 0 { + awsRegion = os.Getenv("AWS_REGION") + } + + if len(awsRegion) == 0 { + return "", fmt.Errorf("%s", "Aws region is not provided") + } + + return awsRegion, nil +} diff --git a/db/cassandra/cassandra_client.go b/db/cassandra/cassandra_client.go index 3804f22..45515aa 100644 --- a/db/cassandra/cassandra_client.go +++ b/db/cassandra/cassandra_client.go @@ -39,6 +39,7 @@ const ( DefaultSleepTimeInMillisecond = 10 DefaultConnections = 2 DefaultPageSize = 50 + DefaultPort = 9042 ) // if 'wifi_schema_v2_enabled'=true, v1.3 is also supported @@ -68,6 +69,19 @@ current column types: */ func NewCassandraClient(conf *configuration.Config, testOnly bool) (*CassandraClient, error) { + if isAwsKeyspaceEnabled(conf) { + return awsKeyspaceClient(conf, testOnly) + } else { + return cassandraClient(conf, testOnly) + } +} + +func isAwsKeyspaceEnabled(conf *configuration.Config) bool { + return (conf.GetString("webconfig.database.active_driver") == "cassandra") && + conf.GetBoolean("webconfig.database.cassandra.aws_keyspace_enabled") +} + +func cassandraClient(conf *configuration.Config, testOnly bool) (*CassandraClient, error) { var codec *security.AesCodec var err error @@ -101,6 +115,7 @@ func NewCassandraClient(conf *configuration.Config, testOnly bool) (*CassandraCl cluster.Timeout = time.Duration(dbconf.GetInt32("timeout_in_sec", 1)) * time.Second cluster.ConnectTimeout = time.Duration(dbconf.GetInt32("connect_timeout_in_sec", 1)) * time.Second cluster.NumConns = int(dbconf.GetInt32("connections", DefaultConnections)) + cluster.Port = int(dbconf.GetInt64("port", DefaultPort)) cluster.RetryPolicy = &gocql.DowngradingConsistencyRetryPolicy{ ConsistencyLevelsToTry: []gocql.Consistency{ diff --git a/go.mod b/go.mod index e085f6d..b84b28e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.21 require ( github.com/IBM/sarama v1.42.1 github.com/MicahParks/keyfunc/v2 v2.1.0 + github.com/aws/aws-sdk-go v1.49.12 + github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0 github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665 github.com/gocql/gocql v1.6.0 github.com/golang-jwt/jwt/v5 v5.0.0 @@ -14,7 +16,6 @@ require ( github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_model v0.2.0 github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.9.0 github.com/twmb/murmur3 v1.1.6 github.com/vmihailenco/msgpack v4.0.4+incompatible github.com/vmihailenco/msgpack/v4 v4.3.12 @@ -53,11 +54,11 @@ require ( github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect @@ -75,5 +76,4 @@ require ( google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 74c2010..07b478c 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,10 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/aws/aws-sdk-go v1.49.12 h1:SbGHDdMjtuTL8zpRXKjvIvQHLt9cCqcxcHoJps23WxI= +github.com/aws/aws-sdk-go v1.49.12/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0 h1:EJsHUYgFBV7/N1YtL73lsfZODAOU+CnNSZfEAlqqQaA= +github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0/go.mod h1:AxKuXHc0zv2yYaeueUG7R3ONbcnQIuDj0bkdFmPVRzU= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -98,6 +102,7 @@ github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gocql/gocql v0.0.0-20200624222514-34081eda590e/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -132,6 +137,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -193,6 +199,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -333,6 +343,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -366,6 +377,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -401,6 +413,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -420,6 +434,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -464,11 +479,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -478,6 +497,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -524,6 +545,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -626,7 +648,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 699b60302673e8ff924b72db25168a91f482588f Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Mon, 13 Jan 2025 11:29:07 +0530 Subject: [PATCH 02/11] Add support to fetch public key using URL to authenticate requests from CPE --- config/sample_webconfig.conf | 16 ++++++++-- security/token.go | 58 +++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index 3126a15..aad67f8 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -49,7 +49,7 @@ webconfig { read_timeout_in_secs = 142 max_idle_conns_per_host = 100 keepalive_timeout_in_secs = 30 - host = "https://api.webpa.comcast.net" + host = "https://api-gateway-shared.dev.hgw.yo-digital.com/hgw/tr1d1um" async_poke_enabled = false async_poke_concurrent_calls = 100 api_version = "v2" @@ -118,6 +118,7 @@ webconfig { enabled = true kids = [ "webconfig_key", + "uat" ] } @@ -132,6 +133,14 @@ webconfig { public_key_file = /tmp/sat-themis-201701.pub } + uat { + // Public key will be fetched using the url if provided. + url = "https://themis/keys/{kid}" + + // If themis_url is not provided, public_key_file will be used. + public_key_file = /tmp/sat-themis-201701.pub + } + sat-prod-k1-1024 { public_key_file = /tmp/sat-prod-k1-1024.pub } @@ -141,7 +150,7 @@ webconfig { enabled = false } device_api_token_auth { - enabled = false + enabled = true } } @@ -152,6 +161,7 @@ webconfig { unittest_db_file = "/tmp/test_webconfig.sqlite" concurrent_queries = 5 } + cassandra { encrypted_password = "" hosts = [ @@ -166,7 +176,7 @@ webconfig { page_size = 50 user = "dbuser" test_keyspace = "test_webconfig" - is_ssl_enabled = true + is_ssl_enabled = false port = 9042 //Config to create database client to AWS Keyspace using IAM temporary credentials diff --git a/security/token.go b/security/token.go index 47270a4..76e9b31 100644 --- a/security/token.go +++ b/security/token.go @@ -19,9 +19,13 @@ package security import ( "crypto/rsa" + "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" "fmt" + "io" + "net/http" "os" "strings" "time" @@ -70,7 +74,16 @@ func NewTokenManager(conf *configuration.Config) *TokenManager { decodeKeys := map[string]*rsa.PublicKey{} for _, kid := range kids { keyfile := conf.GetString(fmt.Sprintf("webconfig.jwt.kid.%s.public_key_file", kid)) - dk, err := loadDecodeKey(keyfile) + var dk *rsa.PublicKey + var err error + + publicKeyUrl := conf.GetString(fmt.Sprintf("webconfig.jwt.kid.%s.url", kid)) + if len(publicKeyUrl) > 0 { + dk, err = fetchPublicKeyFromURL(publicKeyUrl) + } else { + dk, err = loadDecodeKey(keyfile) + } + if err != nil { if panicExitEnabled { panic(err) @@ -104,6 +117,49 @@ func NewTokenManager(conf *configuration.Config) *TokenManager { } } +func fetchPublicKeyFromURL(publicKeyUrl string) (*rsa.PublicKey, error) { + resp, err := http.Get(publicKeyUrl) + if err != nil { + return nil, common.NewError(fmt.Errorf("failed to call Themis endpoint: %v", err)) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, common.NewError(fmt.Errorf("themis service returned status code %d", resp.StatusCode)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, common.NewError(fmt.Errorf("failed to read themis response body: %v", err)) + } + + publicKey, err := parsePublicKey(body) + if err != nil { + return nil, common.NewError(fmt.Errorf("failed to parse themis public key: %v", err)) + } + + return publicKey, nil +} + +func parsePublicKey(pemData []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(pemData) + if block == nil || block.Type != "PUBLIC KEY" { + return nil, common.NewError(fmt.Errorf("failed to decode PEM block containing public key")) + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, common.NewError(fmt.Errorf("failed to parse public key: %v", err)) + } + + rsaPub, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, common.NewError(fmt.Errorf("public key is not of type RSA")) + } + + return rsaPub, nil +} + func loadDecodeKey(keyfile string) (*rsa.PublicKey, error) { kbytes, err := os.ReadFile(keyfile) if err != nil { From b783098c336e513198aa4e01ce2191279fb1671e Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Mon, 13 Jan 2025 12:17:51 +0530 Subject: [PATCH 03/11] revert host config --- config/sample_webconfig.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index aad67f8..f7c60da 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -49,7 +49,7 @@ webconfig { read_timeout_in_secs = 142 max_idle_conns_per_host = 100 keepalive_timeout_in_secs = 30 - host = "https://api-gateway-shared.dev.hgw.yo-digital.com/hgw/tr1d1um" + host = "https://api.webpa.comcast.net" async_poke_enabled = false async_poke_concurrent_calls = 100 api_version = "v2" From 41572d6162e9f53eb3926baa777db0a68d4f695f Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Fri, 17 Jan 2025 14:04:24 +0530 Subject: [PATCH 04/11] Add support of AWS KMS to encrypt/decrpyt data while performing operations in database --- config/sample_webconfig.conf | 25 +++++-- db/cassandra/document.go | 29 ++++---- db/cassandra/schema.go | 9 ++- security/aes_codec.go | 136 +++++++++++++++++++++++++++++------ security/kms_client.go | 58 +++++++++++++++ 5 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 security/kms_client.go diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index f7c60da..cc9464b 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -1,6 +1,22 @@ webconfig { security { + // If encryption_mechanism is not provided, encryption_key_env_name will be used to encrypt data in database + encryption_mechanism = "" encryption_key_env_name = "WEBCONFIG_KEY" + kms { + aws_region = "" + endpoint = "" + secret_key = "" + encryption_algorithm = "" + + role_based_access_enabled = false + + // If role_based_access_enabled is true, access_key_id, secret_access_key and session_token will be fetched using IAM temporary credentials + access_key_id = "" + secret_access_key = "" + // Token is only required for temporary security credentials retrieved via STS, otherwise an empty string can be passed for this parameter. + session_token = "" + } } panic_exit_enabled = false @@ -118,7 +134,6 @@ webconfig { enabled = true kids = [ "webconfig_key", - "uat" ] } @@ -133,12 +148,12 @@ webconfig { public_key_file = /tmp/sat-themis-201701.pub } - uat { + kid { // Public key will be fetched using the url if provided. - url = "https://themis/keys/{kid}" + url = "" // If themis_url is not provided, public_key_file will be used. - public_key_file = /tmp/sat-themis-201701.pub + public_key_file = "/tmp/sat-themis-201701.pub" } sat-prod-k1-1024 { @@ -150,7 +165,7 @@ webconfig { enabled = false } device_api_token_auth { - enabled = true + enabled = false } } diff --git a/db/cassandra/document.go b/db/cassandra/document.go index df580b8..1727171 100644 --- a/db/cassandra/document.go +++ b/db/cassandra/document.go @@ -14,24 +14,24 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 -*/ + */ package cassandra import ( "fmt" "time" + "github.com/gocql/gocql" + "github.com/prometheus/client_golang/prometheus" "github.com/rdkcentral/webconfig/common" "github.com/rdkcentral/webconfig/db" "github.com/rdkcentral/webconfig/util" - "github.com/gocql/gocql" - "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" ) func (c *CassandraClient) GetSubDocument(cpeMac string, groupId string) (*common.SubDocument, error) { var err error - var payload []byte + var payload, kmsRemoteDataKey []byte var version, errorDetails string var state, errorCode int var updatedTime, expiry time.Time @@ -40,8 +40,8 @@ func (c *CassandraClient) GetSubDocument(cpeMac string, groupId string) (*common c.concurrentQueries <- true defer func() { <-c.concurrentQueries }() - stmt := "SELECT payload,version,state,updated_time,error_code,error_details,expiry FROM xpc_group_config WHERE cpe_mac=? AND group_id=?" - if err := c.Query(stmt, cpeMac, groupId).Scan(&payload, &version, &state, &updatedTime, &errorCode, &errorDetails, &expiry); err != nil { + stmt := "SELECT payload,version,state,updated_time,error_code,error_details,expiry,kms_remote_data_key FROM xpc_group_config WHERE cpe_mac=? AND group_id=?" + if err := c.Query(stmt, cpeMac, groupId).Scan(&payload, &version, &state, &updatedTime, &errorCode, &errorDetails, &expiry, &kmsRemoteDataKey); err != nil { return nil, common.NewError(err) } @@ -50,7 +50,7 @@ func (c *CassandraClient) GetSubDocument(cpeMac string, groupId string) (*common } if c.IsEncryptedGroup(groupId) { - payload, err = c.DecryptBytes(payload) + payload, err = c.DecryptBytes(payload, kmsRemoteDataKey) if err != nil { return nil, common.NewError(err) } @@ -113,12 +113,17 @@ func (c *CassandraClient) SetSubDocument(cpeMac string, groupId string, subdoc * columns = append(columns, "payload") // TODO evel if it is necessary use a list of groupIds that need encryption if c.IsEncryptedGroup(groupId) { - encbytes, err := c.EncryptBytes(subdoc.Payload()) + encbytes, kmsRemoteDataKey, err := c.EncryptBytes(subdoc.Payload()) if err != nil { return common.NewError(err) } values = append(values, encbytes) columnMap["payload_len"] = len(encbytes) + + if len(kmsRemoteDataKey) > 0 { + columns = append(columns, "kms_remote_data_key") + values = append(values, kmsRemoteDataKey) + } } else { values = append(values, subdoc.Payload()) columnMap["payload_len"] = len(subdoc.Payload()) @@ -226,7 +231,7 @@ func (c *CassandraClient) GetDocument(cpeMac string, xargs ...interface{}) (fndo c.concurrentQueries <- true defer func() { <-c.concurrentQueries }() - stmt := "SELECT group_id,payload,version,state,updated_time,error_code,error_details,expiry FROM xpc_group_config WHERE cpe_mac=?" + stmt := "SELECT group_id,payload,version,state,updated_time,error_code,error_details,expiry,kms_remote_data_key FROM xpc_group_config WHERE cpe_mac=?" iter := c.Query(stmt, cpeMac).Iter() rmap := make(util.Dict) defer func() { @@ -250,13 +255,13 @@ func (c *CassandraClient) GetDocument(cpeMac string, xargs ...interface{}) (fndo now := time.Now() for { var err error - var payload []byte + var payload, kmsRemoteDataKey []byte var groupId, version, errorDetails string var state, errorCode int var updatedTime, expiry time.Time var updatedTimeTsPtr *int - if !iter.Scan(&groupId, &payload, &version, &state, &updatedTime, &errorCode, &errorDetails, &expiry) { + if !iter.Scan(&groupId, &payload, &version, &state, &updatedTime, &errorCode, &errorDetails, &expiry, &kmsRemoteDataKey) { break } @@ -280,7 +285,7 @@ func (c *CassandraClient) GetDocument(cpeMac string, xargs ...interface{}) (fndo } if c.IsEncryptedGroup(groupId) { - payload, err = c.DecryptBytes(payload) + payload, err = c.DecryptBytes(payload, kmsRemoteDataKey) if err != nil { tfields := common.FilterLogFields(fields) tfields["logger"] = "subdoc" diff --git a/db/cassandra/schema.go b/db/cassandra/schema.go index 6953e84..8882199 100644 --- a/db/cassandra/schema.go +++ b/db/cassandra/schema.go @@ -33,7 +33,8 @@ var ( state int, updated_time timestamp, version text, - PRIMARY KEY (cpe_mac, group_id) + PRIMARY KEY (cpe_mac, group_id), + kms_remote_data_key blob )`, `CREATE TABLE IF NOT EXISTS root_document ( cpe_mac text PRIMARY KEY, @@ -45,12 +46,14 @@ var ( query_params text, route text, schema_version text, - version text + version text, + kms_remote_data_key blob )`, `CREATE TABLE IF NOT EXISTS reference_document ( ref_id text PRIMARY KEY, payload blob, - version text + version text, + kms_remote_data_key blob )`, } diff --git a/security/aes_codec.go b/security/aes_codec.go index e59eb7d..9e23852 100644 --- a/security/aes_codec.go +++ b/security/aes_codec.go @@ -14,7 +14,7 @@ * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 -*/ + */ /* * Some code in Encrypt/Decrypt is: * Copyright 2012 The Go Authors. All rights reserved. @@ -33,8 +33,10 @@ import ( "io" "os" - "github.com/rdkcentral/webconfig/common" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kms" "github.com/go-akka/configuration" + "github.com/rdkcentral/webconfig/common" log "github.com/sirupsen/logrus" ) @@ -66,7 +68,14 @@ remove the first 20 bytes, the "digest" part ==> plaintext */ type AesCodec struct { - key []byte + key []byte + awsKms AwsKms +} + +type AwsKms struct { + kmsClient *kms.KMS + secretKey string + encryptionAlgorithm string } const ( @@ -77,9 +86,36 @@ const ( // var staticIv []byte{111, 114, 219, 23, 120, 151, 157, 32, 117, 31, 98, 99, 106, 3, 169, 224} func NewAesCodec(conf *configuration.Config, args ...string) (*AesCodec, error) { - envName := conf.GetString("webconfig.security.encryption_key_env_name", envNameDefault) + var ( + aesCodec AesCodec + encryptionMechanism = conf.GetString("webconfig.security.encryption_mechanism") + ) + + if encryptionMechanism == "aws_kms" { + kmsClient, err := NewKmsClient(conf) + if err != nil { + return &aesCodec, common.NewError(err) + } + + aesCodec.awsKms = AwsKms{ + kmsClient: kmsClient, + secretKey: conf.GetString("webconfig.security.kms.secret_key"), + encryptionAlgorithm: conf.GetString("webconfig.security.kms.encryption_algorithm"), + } + } else { + key, err := getEncryptionKey(conf, args...) + if err != nil { + return &aesCodec, common.NewError(err) + } + + aesCodec.key = key + } - var defaultCodec AesCodec + return &aesCodec, nil +} + +func getEncryptionKey(conf *configuration.Config, args ...string) ([]byte, error) { + envName := conf.GetString("webconfig.security.encryption_key_env_name", envNameDefault) var enckeyB64 string if len(args) > 0 { @@ -89,18 +125,10 @@ func NewAesCodec(conf *configuration.Config, args ...string) (*AesCodec, error) } if len(enckeyB64) == 0 { - err := fmt.Errorf("No env %v", envName) - return &defaultCodec, common.NewError(err) + return nil, common.NewError(fmt.Errorf("no env %v", envName)) } - key, err := base64.StdEncoding.DecodeString(enckeyB64) - if err != nil { - return &defaultCodec, common.NewError(err) - } - - return &AesCodec{ - key: key, - }, nil + return base64.StdEncoding.DecodeString(enckeyB64) } func (c *AesCodec) Decrypt(encryptedB64 string) (string, error) { @@ -235,7 +263,15 @@ func Padding(ciphertext []byte, blockSize int) []byte { } } -func (c *AesCodec) DecryptBytes(encbytes []byte) ([]byte, error) { +func (c *AesCodec) DecryptBytes(encbytes []byte, kmsRemoteDataKey []byte) ([]byte, error) { + if (c.awsKms != AwsKms{}) { + return c.decryptBytesUsingKMS(encbytes, kmsRemoteDataKey) + } else { + return c.decryptBytesUsingKey(encbytes) + } +} + +func (c *AesCodec) decryptBytesUsingKey(encbytes []byte) ([]byte, error) { var ciphertext []byte block, err := aes.NewCipher(c.key) @@ -260,7 +296,6 @@ func (c *AesCodec) DecryptBytes(encbytes []byte) ([]byte, error) { mode.CryptBlocks(ciphertext, ciphertext) - // unpadding index := len(ciphertext) - 1 for { @@ -279,6 +314,32 @@ func (c *AesCodec) DecryptBytes(encbytes []byte) ([]byte, error) { return decrypted, nil } +func (c *AesCodec) decryptBytesUsingKMS(encbytes []byte, kmsRemoteDataKey []byte) ([]byte, error) { + resp, err := c.awsKms.kmsClient.Decrypt(&kms.DecryptInput{ + CiphertextBlob: kmsRemoteDataKey, + }) + if err != nil { + return nil, fmt.Errorf("failed to decrypt key: %w", err) + } + + block, err := aes.NewCipher(resp.Plaintext) + if err != nil { + return nil, fmt.Errorf("failed to create cipher block: %w", err) + } + + nonce := make([]byte, 12) + stream, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + decodedBytes, err := stream.Open(nil, nonce, encbytes, nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt data: %w", err) + } + return decodedBytes, nil +} + func DigestBytes(iv []byte, plainbytes []byte) []byte { buffer := bytes.NewBuffer(iv) buffer.Write(plainbytes) @@ -288,17 +349,25 @@ func DigestBytes(iv []byte, plainbytes []byte) []byte { return bs } -func (c *AesCodec) EncryptBytes(plainbytes []byte) ([]byte, error) { +func (c *AesCodec) EncryptBytes(plainbytes []byte) ([]byte, []byte, error) { + if (c.awsKms != AwsKms{}) { + return c.encryptBytesUsingKMS(plainbytes) + } else { + return c.encryptBytesUsingKey(plainbytes) + } +} + +func (c *AesCodec) encryptBytesUsingKey(plainbytes []byte) ([]byte, []byte, error) { var ciphertext []byte block, err := aes.NewCipher(c.key) if err != nil { - return ciphertext, err + return ciphertext, nil, err } iv := make([]byte, aes.BlockSize) if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return ciphertext, err + return ciphertext, nil, err } hashed := DigestBytes(iv, plainbytes) @@ -317,11 +386,34 @@ func (c *AesCodec) EncryptBytes(plainbytes []byte) ([]byte, error) { mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext[aes.BlockSize:], hashedIvPlain) - return ciphertext, nil + return ciphertext, nil, nil +} + +func (c *AesCodec) encryptBytesUsingKMS(plainbytes []byte) ([]byte, []byte, error) { + resp, err := c.awsKms.kmsClient.GenerateDataKey(&kms.GenerateDataKeyInput{ + KeyId: aws.String(c.awsKms.secretKey), + KeySpec: aws.String(c.awsKms.encryptionAlgorithm), + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate data key: %w", err) + } + + block, err := aes.NewCipher(resp.Plaintext) + if err != nil { + return nil, nil, fmt.Errorf("failed to create cipher block: %w", err) + } + + nonce := make([]byte, 12) + stream, err := cipher.NewGCM(block) + if err != nil { + return nil, nil, fmt.Errorf("failed to create GCM: %w", err) + } + + return stream.Seal(nil, nonce, plainbytes, nil), resp.CiphertextBlob, nil } func (c *AesCodec) LogResponseDebug(fields log.Fields, bbytes []byte) { - encbytes, err := c.EncryptBytes(bbytes) + encbytes, _, err := c.EncryptBytes(bbytes) if err != nil { log.WithFields(fields).Error(err.Error()) return diff --git a/security/kms_client.go b/security/kms_client.go new file mode 100644 index 0000000..f26a7e4 --- /dev/null +++ b/security/kms_client.go @@ -0,0 +1,58 @@ +package security + +import ( + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/go-akka/configuration" +) + +func NewKmsClient(conf *configuration.Config) (*kms.KMS, error) { + awsRegion, err := getAwsRegionForCassandra(conf) + if err != nil { + return nil, err + } + + awsEndpoint := conf.GetString("webconfig.security.kms.endpoint") + if len(awsEndpoint) == 0 { + return nil, fmt.Errorf("%s", "AWS KMS endpoint is not provided") + } + + awsConfig := &aws.Config{ + Region: aws.String(awsRegion), + Endpoint: aws.String(awsEndpoint), + } + + roleBasedAccessEnabled := conf.GetBoolean("webconfig.security.kms.role_based_access_enabled") + if !roleBasedAccessEnabled { + accessKeyId := conf.GetString("webconfig.security.kms.access_key_id") + secretAccessKey := conf.GetString("webconfig.security.kms.secret_access_key") + sessionToken := conf.GetString("webconfig.security.kms.session_token") + awsConfig.Credentials = credentials.NewStaticCredentials(accessKeyId, secretAccessKey, sessionToken) + } + + sess, err := session.NewSession(awsConfig) + if err != nil { + return nil, err + } + + return kms.New(sess), nil +} + +func getAwsRegionForCassandra(conf *configuration.Config) (string, error) { + awsRegion := conf.GetString("webconfig.security.kms.aws_region") + + if len(awsRegion) == 0 { + awsRegion = os.Getenv("AWS_REGION") + } + + if len(awsRegion) == 0 { + return "", fmt.Errorf("%s", "AWS region for KMS is not provided") + } + + return awsRegion, nil +} From 9bae5f8a590d5726bf0afd92e1b3b1819d82b4fe Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Fri, 17 Jan 2025 17:02:18 +0530 Subject: [PATCH 05/11] refactor code --- db/cassandra/document.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/cassandra/document.go b/db/cassandra/document.go index 1727171..16d7e20 100644 --- a/db/cassandra/document.go +++ b/db/cassandra/document.go @@ -120,7 +120,7 @@ func (c *CassandraClient) SetSubDocument(cpeMac string, groupId string, subdoc * values = append(values, encbytes) columnMap["payload_len"] = len(encbytes) - if len(kmsRemoteDataKey) > 0 { + if kmsRemoteDataKey != nil { columns = append(columns, "kms_remote_data_key") values = append(values, kmsRemoteDataKey) } From 3dff51ff825ada446defa1165ab229a06176cb8a Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Tue, 21 Jan 2025 13:39:07 +0530 Subject: [PATCH 06/11] Refactor code to handle decode keys map creation --- security/token.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/security/token.go b/security/token.go index 76e9b31..080d68c 100644 --- a/security/token.go +++ b/security/token.go @@ -90,8 +90,9 @@ func NewTokenManager(conf *configuration.Config) *TokenManager { } else { fmt.Printf("WARNING %v\n", err) } + } else { + decodeKeys[kid] = dk } - decodeKeys[kid] = dk } fn := VerifyToken @@ -120,22 +121,22 @@ func NewTokenManager(conf *configuration.Config) *TokenManager { func fetchPublicKeyFromURL(publicKeyUrl string) (*rsa.PublicKey, error) { resp, err := http.Get(publicKeyUrl) if err != nil { - return nil, common.NewError(fmt.Errorf("failed to call Themis endpoint: %v", err)) + return nil, common.NewError(fmt.Errorf("unable to fetch the public key from URL: %v", err)) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, common.NewError(fmt.Errorf("themis service returned status code %d", resp.StatusCode)) + return nil, common.NewError(fmt.Errorf("unexpected status code %d received while fetching the public key from URL", resp.StatusCode)) } body, err := io.ReadAll(resp.Body) if err != nil { - return nil, common.NewError(fmt.Errorf("failed to read themis response body: %v", err)) + return nil, common.NewError(fmt.Errorf("failed to read the response body from public key URL: %v", err)) } publicKey, err := parsePublicKey(body) if err != nil { - return nil, common.NewError(fmt.Errorf("failed to parse themis public key: %v", err)) + return nil, common.NewError(fmt.Errorf("error parsing the public key fetched from URL: %v", err)) } return publicKey, nil From 8846653ca5950a3cf3f5f9d5e4f8318c6c42edcd Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Tue, 21 Jan 2025 18:08:31 +0530 Subject: [PATCH 07/11] remove kms endpoint validation if not provided --- security/kms_client.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/security/kms_client.go b/security/kms_client.go index f26a7e4..1d2d736 100644 --- a/security/kms_client.go +++ b/security/kms_client.go @@ -17,14 +17,9 @@ func NewKmsClient(conf *configuration.Config) (*kms.KMS, error) { return nil, err } - awsEndpoint := conf.GetString("webconfig.security.kms.endpoint") - if len(awsEndpoint) == 0 { - return nil, fmt.Errorf("%s", "AWS KMS endpoint is not provided") - } - awsConfig := &aws.Config{ Region: aws.String(awsRegion), - Endpoint: aws.String(awsEndpoint), + Endpoint: aws.String(conf.GetString("webconfig.security.kms.endpoint")), } roleBasedAccessEnabled := conf.GetBoolean("webconfig.security.kms.role_based_access_enabled") From b5538b621a2ec0377d54c264a9dbdf68d398fc38 Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Fri, 7 Feb 2025 12:47:20 +0530 Subject: [PATCH 08/11] Add awsKeyspaceEnabled boolean in CassandraClient struct --- db/cassandra/aws_keyspace.go | 1 + db/cassandra/cassandra_client.go | 1 + db/cassandra/document.go | 3 +++ 3 files changed, 5 insertions(+) diff --git a/db/cassandra/aws_keyspace.go b/db/cassandra/aws_keyspace.go index 320264d..23d896b 100644 --- a/db/cassandra/aws_keyspace.go +++ b/db/cassandra/aws_keyspace.go @@ -126,6 +126,7 @@ func awsKeyspaceClient(conf *configuration.Config, testOnly bool) (*CassandraCli encryptedSubdocIds: encryptedSubdocIds, stateCorrectionEnabled: stateCorrectionEnabled, lockRootDocumentEnabled: lockRootDocumentEnabled, + awsKeyspaceEnabled: true, }, nil } diff --git a/db/cassandra/cassandra_client.go b/db/cassandra/cassandra_client.go index 45515aa..9857279 100644 --- a/db/cassandra/cassandra_client.go +++ b/db/cassandra/cassandra_client.go @@ -55,6 +55,7 @@ type CassandraClient struct { encryptedSubdocIds []string stateCorrectionEnabled bool lockRootDocumentEnabled bool + awsKeyspaceEnabled bool } /* diff --git a/db/cassandra/document.go b/db/cassandra/document.go index 16d7e20..37abd04 100644 --- a/db/cassandra/document.go +++ b/db/cassandra/document.go @@ -232,6 +232,9 @@ func (c *CassandraClient) GetDocument(cpeMac string, xargs ...interface{}) (fndo defer func() { <-c.concurrentQueries }() stmt := "SELECT group_id,payload,version,state,updated_time,error_code,error_details,expiry,kms_remote_data_key FROM xpc_group_config WHERE cpe_mac=?" + if c.awsKeyspaceEnabled { + stmt += " ALLOW FILTERING" + } iter := c.Query(stmt, cpeMac).Iter() rmap := make(util.Dict) defer func() { From d98835310f0ab43cb79e7ce3505df8de85bf8ff8 Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Fri, 7 Feb 2025 18:14:40 +0530 Subject: [PATCH 09/11] Add ALLOW FILTERING in Delete Document Query --- db/cassandra/document.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/db/cassandra/document.go b/db/cassandra/document.go index 37abd04..3d33ba6 100644 --- a/db/cassandra/document.go +++ b/db/cassandra/document.go @@ -205,6 +205,9 @@ func (c *CassandraClient) DeleteDocument(cpeMac string) error { defer func() { <-c.concurrentQueries }() stmt := "DELETE FROM xpc_group_config WHERE cpe_mac=?" + if c.awsKeyspaceEnabled { + stmt += " ALLOW FILTERING" + } if err := c.Query(stmt, cpeMac).Exec(); err != nil { return common.NewError(err) } From 274d18c4d72cafa6bf288cb7e1c2538ac4c9e983 Mon Sep 17 00:00:00 2001 From: Shivam Kumar <31433308+shivam4035@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:48:02 +0530 Subject: [PATCH 10/11] Modify code to delete document by CPE mac if AWS Keyspace enabled (#1) * testing * testing * Update multipart.go * Updated code for deleting from cpeMac * Testing * Testing * Updated code to delete data for cpe mac --------- Co-authored-by: ChaitanyaSingla Co-authored-by: shivamkumar --- db/cassandra/document.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/db/cassandra/document.go b/db/cassandra/document.go index 3d33ba6..de37701 100644 --- a/db/cassandra/document.go +++ b/db/cassandra/document.go @@ -204,12 +204,24 @@ func (c *CassandraClient) DeleteDocument(cpeMac string) error { c.concurrentQueries <- true defer func() { <-c.concurrentQueries }() - stmt := "DELETE FROM xpc_group_config WHERE cpe_mac=?" if c.awsKeyspaceEnabled { - stmt += " ALLOW FILTERING" - } - if err := c.Query(stmt, cpeMac).Exec(); err != nil { - return common.NewError(err) + stmt := "SELECT group_id FROM xpc_group_config WHERE cpe_mac=? ALLOW FILTERING" + iter := c.Query(stmt, cpeMac).Iter() + for { + var groupId string + if !iter.Scan(&groupId) { + break + } + stmt := "DELETE FROM xpc_group_config WHERE cpe_mac=? AND group_id=?" + if err := c.Query(stmt, cpeMac, groupId).Exec(); err != nil { + return common.NewError(err) + } + } + } else { + stmt := "DELETE FROM xpc_group_config WHERE cpe_mac=?" + if err := c.Query(stmt, cpeMac).Exec(); err != nil { + return common.NewError(err) + } } return nil From 0d60c036ab6e1d0ddd9d8d877da5a7af141aa5e3 Mon Sep 17 00:00:00 2001 From: ChaitanyaSingla Date: Wed, 2 Apr 2025 12:18:48 +0530 Subject: [PATCH 11/11] Refactor config --- config/sample_webconfig.conf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index cc9464b..20c48a4 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -1,6 +1,7 @@ webconfig { security { - // If encryption_mechanism is not provided, encryption_key_env_name will be used to encrypt data in database + // If encryption_mechanism is aws_kms, AWS KMS will be used to encrypt data in database, else + // encryption_key_env_name will be used. encryption_mechanism = "" encryption_key_env_name = "WEBCONFIG_KEY" kms { @@ -152,7 +153,7 @@ webconfig { // Public key will be fetched using the url if provided. url = "" - // If themis_url is not provided, public_key_file will be used. + // If url is not provided, public_key_file will be used. public_key_file = "/tmp/sat-themis-201701.pub" } @@ -191,7 +192,7 @@ webconfig { page_size = 50 user = "dbuser" test_keyspace = "test_webconfig" - is_ssl_enabled = false + is_ssl_enabled = true port = 9042 //Config to create database client to AWS Keyspace using IAM temporary credentials