Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Doc encryption with symmetric key #2731

Merged
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e58f2d6
Add encryption package
islamaliev Jun 12, 2024
107a7f3
Pass enc key upon doc creation
islamaliev Jun 13, 2024
3bc618b
Add license
islamaliev Jun 14, 2024
09c538e
Slight restructure
islamaliev Jun 14, 2024
9c8b79e
Add Encstore to Rootstore
islamaliev Jun 14, 2024
5d64820
Pass store to encryptor
islamaliev Jun 16, 2024
579920b
Store encKey and read from storage
islamaliev Jun 17, 2024
5498a64
Add p2p test
islamaliev Jun 17, 2024
5396df7
Lint
islamaliev Jun 17, 2024
e36664c
Make defra generate doc encryption key
islamaliev Jun 18, 2024
fc4664d
Remove unused code
islamaliev Jun 18, 2024
fe7c5c2
Store enc flat in a block. On update read the flat from a prev block
islamaliev Jun 20, 2024
0f27447
Fix p2p side
islamaliev Jun 21, 2024
e321f84
Upon peer sync update only heads
islamaliev Jun 24, 2024
2f83d92
Follow up
islamaliev Jun 24, 2024
13c9e9c
Polish
islamaliev Jun 24, 2024
e326c59
Add tests for encryptor
islamaliev Jun 24, 2024
c86dc60
Polish
islamaliev Jun 25, 2024
dfc104b
Encrypt counter CRDT fields
islamaliev Jun 25, 2024
35fa1fa
Fix lint
islamaliev Jun 25, 2024
3ed6435
Update docs
islamaliev Jun 25, 2024
e3135f2
CreateMany tests action
islamaliev Jun 26, 2024
662e8e1
Enable CreateMany in integration tests
islamaliev Jun 26, 2024
bb3c506
Adjust CLI client
islamaliev Jun 26, 2024
8cdc986
Roll back some prev change
islamaliev Jun 26, 2024
bb9ce22
Set Block.IsEncrypted only if true
islamaliev Jun 26, 2024
1fc60f7
Polish
islamaliev Jun 27, 2024
6d90c1f
Fix CreateMany for QGL mutation type
islamaliev Jun 27, 2024
0ba0a96
Fix GQL mutation
islamaliev Jun 27, 2024
bfffa98
Update docs
islamaliev Jun 27, 2024
ca1bda7
Add encConf context upon http CreateMany
islamaliev Jun 27, 2024
485f5fc
Add separate regular CreateMany test
islamaliev Jun 27, 2024
b812dc8
Make create mutation accept array of docs
islamaliev Jun 28, 2024
e90a959
Add encrypt param to create gql mutation
islamaliev Jun 28, 2024
487a48c
Polish
islamaliev Jun 28, 2024
f252cdd
Remove superfluous qql schema type records
islamaliev Jun 30, 2024
1cbab95
Polish docs
islamaliev Jul 1, 2024
225eedf
Remove unnecessary cli flag
islamaliev Jul 1, 2024
592dfdb
Fix cli handling of CreateMany
islamaliev Jul 1, 2024
87574dc
Set query param instead of header
islamaliev Jul 2, 2024
9a99d3f
Use regexp to determine if json bytes is array
islamaliev Jul 3, 2024
8bc56c7
Code review fixups
islamaliev Jul 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion cli/collection_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ import (
"github.com/spf13/cobra"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/internal/db"
)

func MakeCollectionCreateCommand() *cobra.Command {
var file string
var shouldEncrypt bool
var cmd = &cobra.Command{
Use: "create [-i --identity] <document>",
Use: "create [-i --identity] [-e --encrypt] <document>",
islamaliev marked this conversation as resolved.
Show resolved Hide resolved
Short: "Create a new document.",
Long: `Create a new document.

Options:
-i, --identity
Marks the document as private and set the identity as the owner. The access to the document
and permissions are controlled by ACP (Access Control Policy).

-e, --encrypt
Encrypt flag specified if the document needs to be encrypted. If set, DefraDB will generate a
symmetric key for encryption using AES-GCM.
islamaliev marked this conversation as resolved.
Show resolved Hide resolved
islamaliev marked this conversation as resolved.
Show resolved Hide resolved

Example: create from string:
defradb client collection create --name User '{ "name": "Bob" }'
Expand Down Expand Up @@ -69,6 +80,9 @@ Example: create from stdin:
return cmd.Usage()
}

txn, _ := db.TryGetContextTxn(cmd.Context())
setContextDocEncryption(cmd, shouldEncrypt, txn)

if client.IsJSONArray(docData) {
docs, err := client.NewDocsFromJSON(docData, col.Definition())
if err != nil {
Expand All @@ -84,6 +98,8 @@ Example: create from stdin:
return col.Create(cmd.Context(), doc)
},
}
cmd.PersistentFlags().BoolVarP(&shouldEncrypt, "encrypt", "e", false,
"Flag to enable encryption of the document")
cmd.Flags().StringVarP(&file, "file", "f", "", "File containing document(s)")
return cmd
}
15 changes: 15 additions & 0 deletions cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@

acpIdentity "github.com/sourcenetwork/defradb/acp/identity"
"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/datastore"
"github.com/sourcenetwork/defradb/http"
"github.com/sourcenetwork/defradb/internal/db"
"github.com/sourcenetwork/defradb/internal/encryption"
"github.com/sourcenetwork/defradb/keyring"
)

Expand Down Expand Up @@ -160,6 +162,19 @@
return nil
}

// setContextDocEncryption sets doc encryption for the current command context.
func setContextDocEncryption(cmd *cobra.Command, shouldEncrypt bool, txn datastore.Txn) {
if !shouldEncrypt {
return
}
ctx := cmd.Context()
if txn != nil {
ctx = encryption.ContextWithStore(ctx, txn)

Check warning on line 172 in cli/utils.go

View check run for this annotation

Codecov / codecov/patch

cli/utils.go#L172

Added line #L172 was not covered by tests
}
ctx = encryption.SetContextConfig(ctx, encryption.DocEncConfig{IsEncrypted: true})
cmd.SetContext(ctx)
}

// setContextRootDir sets the rootdir for the current command context.
func setContextRootDir(cmd *cobra.Command) error {
rootdir, err := cmd.Root().PersistentFlags().GetString("rootdir")
Expand Down
7 changes: 3 additions & 4 deletions client/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ package client
import (
"encoding/json"
"errors"
"regexp"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -118,11 +117,11 @@ func NewDocFromMap(data map[string]any, collectionDefinition CollectionDefinitio
return doc, nil
}

var jsonArrayPattern = regexp.MustCompile(`^\s*\[.*\]\s*$`)

// IsJSONArray returns true if the given byte array is a JSON Array.
func IsJSONArray(obj []byte) bool {
return jsonArrayPattern.Match(obj)
var js []any
err := json.Unmarshal(obj, &js)
return err == nil
islamaliev marked this conversation as resolved.
Show resolved Hide resolved
}

// NewFromJSON creates a new instance of a Document from a raw JSON object byte array.
Expand Down
62 changes: 62 additions & 0 deletions client/document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,65 @@ func TestNewFromJSON_WithInvalidJSONFieldValueSimpleString_Error(t *testing.T) {
_, err := NewDocFromJSON(objWithJSONField, def)
require.ErrorContains(t, err, "invalid JSON payload. Payload: blah")
}

func TestIsJSONArray(t *testing.T) {
tests := []struct {
name string
input []byte
expected bool
}{
{
name: "Valid JSON Array",
input: []byte(`[{"name":"John","age":21},{"name":"Islam","age":33}]`),
expected: true,
},
{
name: "Valid Empty JSON Array",
input: []byte(`[]`),
expected: true,
},
{
name: "Valid JSON Object",
input: []byte(`{"name":"John","age":21}`),
expected: false,
},
{
name: "Invalid JSON String",
input: []byte(`{"name":"John","age":21`),
expected: false,
},
{
name: "Non-JSON String",
input: []byte(`Hello, World!`),
expected: false,
},
{
name: "Array of Primitives",
input: []byte(`[1, 2, 3, 4]`),
expected: true,
},
{
name: "Nested JSON Array",
input: []byte(`[[1, 2], [3, 4]]`),
expected: true,
},
{
name: "Valid JSON Array with Whitespace",
input: []byte(`
[
{"name": "John", "age": 21},
{"name": "Islam", "age": 33}
]`),
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := IsJSONArray(tt.input)
if actual != tt.expected {
t.Errorf("IsJSONArray(%s) = %v; expected %v", tt.input, actual, tt.expected)
}
})
}
}
3 changes: 3 additions & 0 deletions client/request/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ const (

Cid = "cid"
Input = "input"
Inputs = "inputs"
FieldName = "field"
FieldIDName = "fieldId"
ShowDeleted = "showDeleted"

EncryptArgName = "encrypt"

FilterClause = "filter"
GroupByClause = "groupBy"
LimitClause = "limit"
Expand Down
9 changes: 9 additions & 0 deletions client/request/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ type ObjectMutation struct {
//
// This is ignored for [DeleteObjects] mutations.
Input map[string]any

// Inputs is the array of json representations of the fieldName-value pairs of document
// properties to mutate.
//
// This is ignored for [DeleteObjects] mutations.
Inputs []map[string]any

// Encrypt is a boolean flag that indicates whether the input data should be encrypted.
Encrypt bool
}

// ToSelect returns a basic Select object, with the same Name, Alias, and Fields as
Expand Down
47 changes: 47 additions & 0 deletions datastore/mocks/txn.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions datastore/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ var (
headStoreKey = rootStoreKey.ChildString("heads")
blockStoreKey = rootStoreKey.ChildString("blocks")
peerStoreKey = rootStoreKey.ChildString("ps")
encStoreKey = rootStoreKey.ChildString("enc")
)

type multistore struct {
root DSReaderWriter
data DSReaderWriter
enc DSReaderWriter
islamaliev marked this conversation as resolved.
Show resolved Hide resolved
head DSReaderWriter
peer DSBatching
system DSReaderWriter
Expand All @@ -43,6 +45,7 @@ func MultiStoreFrom(rootstore ds.Datastore) MultiStore {
ms := &multistore{
root: rootRW,
data: prefix(rootRW, dataStoreKey),
enc: prefix(rootRW, encStoreKey),
head: prefix(rootRW, headStoreKey),
peer: namespace.Wrap(rootstore, peerStoreKey),
system: prefix(rootRW, systemStoreKey),
Expand All @@ -57,6 +60,11 @@ func (ms multistore) Datastore() DSReaderWriter {
return ms.data
}

// Encstore implements MultiStore.
func (ms multistore) Encstore() DSReaderWriter {
return ms.enc
}

// Headstore implements MultiStore.
func (ms multistore) Headstore() DSReaderWriter {
return ms.head
Expand Down
4 changes: 4 additions & 0 deletions datastore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ type MultiStore interface {
// under the /data namespace
Datastore() DSReaderWriter

// Encstore is a wrapped root DSReaderWriter
// under the /enc namespace
Encstore() DSReaderWriter
islamaliev marked this conversation as resolved.
Show resolved Hide resolved

// Headstore is a wrapped root DSReaderWriter
// under the /head namespace
Headstore() DSReaderWriter
Expand Down
12 changes: 11 additions & 1 deletion docs/website/references/cli/defradb_client_collection_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ Create a new document.
### Synopsis

Create a new document.

Options:
-i, --identity
Marks the document as private and set the identity as the owner. The access to the document
and permissions are controlled by ACP (Access Control Policy).

-e, --encrypt
Encrypt flag specified if the document needs to be encrypted. If set, DefraDB will generate a
symmetric key for encryption using AES-GCM.

Example: create from string:
defradb client collection create --name User '{ "name": "Bob" }'
Expand All @@ -24,12 +33,13 @@ Example: create from stdin:


```
defradb client collection create [-i --identity] <document> [flags]
defradb client collection create [-i --identity] [-e --encrypt] <document> [flags]
```

### Options

```
-e, --encrypt Flag to enable encryption of the document
-f, --file string File containing document(s)
-h, --help help for create
```
Expand Down
4 changes: 4 additions & 0 deletions http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,16 @@ func (c *Client) ExecRequest(
result.GQL.Errors = []error{err}
return result
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL.String(), bytes.NewBuffer(body))
if err != nil {
result.GQL.Errors = []error{err}
return result
}
err = c.http.setDefaultHeaders(req)

setDocEncryptionFlagIfNeeded(ctx, req)

if err != nil {
result.GQL.Errors = []error{err}
return result
Expand Down
14 changes: 14 additions & 0 deletions http/client_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
sse "github.com/vito/go-sse/sse"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/internal/encryption"
)

var _ client.Collection = (*Collection)(nil)
Expand Down Expand Up @@ -78,6 +79,8 @@ func (c *Collection) Create(
return err
}

setDocEncryptionFlagIfNeeded(ctx, req)

_, err = c.http.request(req)
if err != nil {
return err
Expand Down Expand Up @@ -114,6 +117,8 @@ func (c *Collection) CreateMany(
return err
}

setDocEncryptionFlagIfNeeded(ctx, req)

_, err = c.http.request(req)
if err != nil {
return err
Expand All @@ -125,6 +130,15 @@ func (c *Collection) CreateMany(
return nil
}

func setDocEncryptionFlagIfNeeded(ctx context.Context, req *http.Request) {
encConf := encryption.GetContextConfig(ctx)
if encConf.HasValue() && encConf.Value().IsEncrypted {
q := req.URL.Query()
q.Set(docEncryptParam, "true")
req.URL.RawQuery = q.Encode()
}
}

func (c *Collection) Update(
ctx context.Context,
doc *client.Document,
Expand Down
4 changes: 4 additions & 0 deletions http/client_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
panic("client side transaction")
}

func (c *Transaction) Encstore() datastore.DSReaderWriter {
panic("client side transaction")

Check warning on line 95 in http/client_tx.go

View check run for this annotation

Codecov / codecov/patch

http/client_tx.go#L94-L95

Added lines #L94 - L95 were not covered by tests
}

func (c *Transaction) Headstore() datastore.DSReaderWriter {
panic("client side transaction")
}
Expand Down
Loading