Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Hi All,

Recently at work, I've encountered the need to encrypt the contents of a field in one of my Ent schemas. I worked on a local solution, but I think it would be great to provide something like this as either an extension or a core capability for ent.

In general, the user story goes something like this:

  • As a user I would like to make sure that some field of an entity is encrypted at rest. Aside from initial configuration, I'd like reading and writing to this field to be transparent, as if I'm dealing with an ordinary string/byte slice.
  • As a user, I want to provide the encryption key to Ent at runtime.
  • As a user, I want some sane default for my encryption/decryption function (there are many options, see examples from go std lib). However, I may encounter a use case where I'd like to choose my own cipher/padding scheme/etc.)

So a few questions to the community:

  1. Do you have a similar use case? Are your needs captured by these stories?
  2. Do you have any ideas on what the API should look like?
  3. If you have technical expertise with cryptography, I would love to get your 2 cents on what the default implementation for symmetric encryption should look like.

Thanks in advance!

You must be logged in to vote

Replies: 6 comments · 4 replies

Comment options

Hi @rotemtam,
I want to recommend using tink as an encryption library:

  1. Created by Google and maintained by professionals in security engineering.
  2. Many companies are using it in production.
  3. It has official go bindings.

Since it's an external lib I suggest adding it as an extension.
For defining the security I suggest using annotations:

func (Todo) Fields() []ent.Field {
    return []ent.Field{
        field.Text("text").
            NotEmpty().
            Annotations(
                crypto.Enable(),
                crypto.Algorithem("AEAD"),
                ctypto.KeyType("AES128_GCM",
            ),
    }
}
You must be logged in to vote
0 replies
Comment options

We've been experimenting with approaches to the problem here at @ariga (work done by @yonidavidson). I can share some details on a "Work in Progress" in this field, please don't take it as a final implementation or anything, but perhaps this can be useful to you.

This is for the use case where we need symmetric encryption, we define:

// Cipher is used to encrypt and decrypt information in a field.
type Cipher struct {
	Encrypter
	Decrypter
}

// Encrypter is the interface that wraps the Encrypt method.
type Encrypter interface {
	Encrypt(string) (string, error)
}

// Decrypter is the interface that wraps the Decrypt method.
type Decrypter interface {
	Decrypt(string) (string, error)
}

Then we use ent's External Dependencies feature to inject a crypto.Cipher to the Ent client:

entc.Dependency(
  entc.DependencyName("Cipher"),
  entc.DependencyType(&crypto.Cipher{}),
),

We have some Encrypter and Decrypter implementation that is based on https://github.com/google/tink.

When we initialize the client we pass the implementation in:

	tk := newTinkCrypto(keyHandle)
	cipher := &crypto.Cipher{Encrypter: tk, Decrypter: tk}
	drv := sql.OpenDB(dialect, db)
	client := ent.NewClient(
		ent.Driver(drv),
		ent.Cipher(cipher),
	)

Next, we use the Cipher from within a Schema Hook:

func encryptPass(next ent.Mutator) ent.Mutator {
	return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) {
		if p, ok := info.User.Password(); ok {
			e, err := m.Cipher.Encrypt(p)
			if err != nil {
				return nil, err
			}
			m.SetPassword(e)
		}
		return next.Mutate(ctx, m)
	})
}

So far this guarantees encryption on the write path. Since we don't have anything like interceptors in Ent yet, we place a file such as user_decrypt.go in ent/ with an additional method receiver:

func (u *User) DecryptedPass() (string, error) {
	return u.Cipher.Decrypt(u.Password)
}

Then we can invoke this method to get the clear text version of the password where needed.

This is far from perfect, but I thought it might be helpful to share this work in progress.

You must be logged in to vote
2 replies
@rotemtam
Comment options

Just to add, the goal of this discussion is to come up with a more general API for using schema annotations and an Ent extension to make all of this glue obsolete. I'm really keen to hear what the community has to say about this :-D

@leejuyuu
Comment options

Now that Interceptor has been implemented, the decryption can be done like this:

func decrypt(next ent.Querier) ent.Querier {
	return intercept.UserFunc(func(ctx context.Context, uq *gen.UserQuery) (gen.Value, error) {
		if uq.Cipher == nil {
			return nil, errors.New("decrypt query failed: Cipher is not set in the ent client")
		}

		value, err := next.Query(ctx, uq)
		if err != nil {
			return nil, err
		}

		users, ok := value.([]*gen.User)
		if !ok {
			return nil, errors.New("unexpected query result, expecting []*User")
		}

		for _, u := range users {
			u.Password, err = uq.Cipher.Decrypt(u.Password)
			if err != nil {
				return nil, fmt.Errorf("decrypt password failed: %w", err)
			}
		}
		return users, nil
	})
}
Comment options

I writed an exmpale. The encryption and decryption is processed by GoType. Value function use to Encrypt raw value and store ciphertext to database, and Scan function use to Decrypt ciphertext from database to memory. Decryption and encryption provide by tink.

Example here: https://github.com/seamory/ent-encrypt-example

You must be logged in to vote
2 replies
@rotemtam
Comment options

That's a very cool approach, thanks for sharing @seamory 👍

@seamory
Comment options

Thanks for your affirmation. I'm glad to share my solution. Hoping it's useful to help other developers and development for ent project.

Comment options

Hi @rotemtam
Thanks for inviting me to the discussion here. For simplicity, it would be good to do encryption and decryption in the schema.
I made a simple update based on the crypto package. Maybe a much more complex structure could be prepared, but choosing between simplicity and power would make things easier.
We can look at what other ORM frameworks are doing and get inspired. Since I have been using Doctrine Orm for a long time, I offered a similar solution.

You must be logged in to vote
0 replies
Comment options

Has anyone come up with in the mean-time? We were thinking about using pgsodium. Though that's a very postgres specific approach. Anyway, wanted to ask if there had been developments here.

You must be logged in to vote
0 replies
Comment options

For anyone implementing this up as of today, Tink project is kind of restructured and we should be using this one https://github.com/tink-crypto/tink-go

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
💡
Ideas
Labels
None yet
7 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.