danielberkompas / cloak_ecto Goto Github PK
View Code? Open in Web Editor NEWEncrypted fields for Ecto
License: MIT License
Encrypted fields for Ecto
License: MIT License
I have a schema with one encrypted field and want to rotate the keys, but the schema also has a field :state, Ecto.Enum, values: [:enabled, :disabled, :archived], default: :enabled
field and it is raising an exception:
** (FunctionClauseError) no function clause matching in Code.ensure_loaded?/1
The following arguments were given to Code.ensure_loaded?/1:
# 1
{:parameterized, Ecto.Enum, %{embed_as: :self, mappings: [enabled: "enabled", disabled: "disabled", archived: "archived"], on_cast: %{"archived" => :archived, "disabled" => :disabled, "enabled" => :enabled}, on_dump: %{archived: "archived", disabled: "disabled", enabled: "enabled"}, on_load: %{"archived" => :archived, "disabled" => :disabled, "enabled" => :enabled}, type: :string}}
Attempted function clauses (showing 1 out of 1):
def ensure_loaded?(module) when is_atom(module)
(elixir 1.13.4) lib/code.ex:1472: Code.ensure_loaded?/1
(cloak_ecto 1.2.0) lib/cloak_ecto/migrator.ex:77: Cloak.Ecto.Migrator.cloak_field?/1
(elixir 1.13.4) lib/enum.ex:4034: Enum.filter_list/2
(cloak_ecto 1.2.0) lib/cloak_ecto/migrator.ex:62: Cloak.Ecto.Migrator.cloak_fields/1
I assume a fix would have to be pushed so I'll try looking into it, but if anyone has any ideas in the meantime, it'd be appreciated.
I cannot find any documentation on how to make make hashed, searchable columns to go along with my encrypted column. Any help would be appreciated, thanks for the sweet lib!
Erlang/OTP 24.2 ships with :crypto.pbkdf2_hmac. I'm suggesting a new major version of cloak_ecto
that requires OTP 24.2+.
Now I have a legacy database, a column is encrypted by PGP_SYM_ENCRYPT.
I wonder if I can use cloak_ecto to deal with that?
Hi,
when we wanted to upgrade Ecto to 3.6.0 (diff, changelog but after upgrade it's not possible to compile our projects.
There is error:
= Compilation error in file lib/our_app/models/model.ex ==
** (ArgumentError) argument error
(stdlib 3.14) :ets.lookup(OurApp.Vault.Config, :config)
(cloak 1.0.3) lib/cloak/vault.ex:285: Cloak.Vault.read_config/1
lib/cloak/vault.ex:266: OurApp.Vault.json_library/0
lib/cloak_ecto/types/map.ex:60: OurApp.Vault.Map.before_encrypt/1
lib/cloak_ecto/type.ex:36: OurApp.Vault.Map.dump/1
(ecto 3.6.0) lib/ecto/schema.ex:2091: Ecto.Schema.validate_default!/2
(ecto 3.6.0) lib/ecto/schema.ex:1830: Ecto.Schema.__field__/4
lib/our_app/models/model.ex:19: (module)
(stdlib 3.14) erl_eval.erl:680: :erl_eval.do_apply/6
(elixir 1.11.2) lib/kernel/parallel_compiler.ex:314: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
Looks like it want to load configuration from ETS which is not available at compile time.
We figured out that default value for field is creating this issue.
This works:
field :responses, OurApp.Vault.Map
This doesn't work
field :responses, OurApp.Vault.Map, default: %{}
Config:
defmodule OurApp.Vault do
use Cloak.Vault, otp_app: :our_app
@impl GenServer
def init(config) do
config =
Keyword.put(config, :ciphers,
default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("CLOAK_SECRET")}
)
{:ok, config}
end
defp decode_env!(var), do: System.get_env(var) |> decode!()
defp decode!(nil), do: Application.get_all_env(:our_app)[OurApp.Vault][:key] |> Base.decode64!()
defp decode!(var), do: Base.decode64!(var)
end
Any suggestions to fix this?
The current docs include a usage example which suggests hashing the email field with SHA256 along with a fully encrypted (secure symmetric cipher) version of the field.
By including the SHA256 version of the data, the encryption is rendered virtually useless as the same data is now also stored in the data-base on a much weaker scheme. Storing sensitive data in this way is not secure due to the below reasons and should be removed from the docs:
SHA256([email protected])
then they can use that knowledge to find any other records in the database that correlate with that email addressThese weaknesses may be acceptable for a given application but I think the reader should be warned if you decide to include that example in the docs. And at the very least, SHA256 should be replaced by HMAC with a key known only to the application owner.
Is there any way to make cloak_ecto use a unique key for every tenant?
The runtime configuration Cloak.Ecto.HMAC.init/1
is very useful but can it be invoked per process?
Hello.
I'm trying to implement the same thing that we have during config MyApp.Vault.
For example:
I can have more than one cipher:
Keyword.put(config, :ciphers, [ default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("MY_KEY")}, two_factor_auth: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("MY_SEC_KEY")} ])
If I use MyApp.Vault.encrypt("test", :two_factor_auth)
I can choose which ciphers I want to use.
Unfortunately, I couldn't do the same thing during config a field on my Ecto Schema.
field(:two_factor_token, MyApp.Encrypted.Binary, label: :two_factor_auth)
There is this possibility? To choose which cipher my field should use?
Recently testing the version 1.2.0
, before update the library. I received the error bellow, when I try to persist a new row or read something from Postgres
already encrypt.
Elixir version 1.11.3 + OTP 23
** (ErlangError) Erlang error: {:badarg, {'aead.c', 90}, 'Unknown cipher'}
(crypto 5.0) :crypto.aead_cipher_nif(:aes_256_gcm, <<...>>, <<...>>, <<...>>, "AES256GCM", <<...>>, false)
The tests pass but when I run the application the error happens.
The migrator ensures that every type on the schema is loaded, however types such as {:array, :integer}
are not modules, and so cause the migrator to crash.
The code in question is here: https://github.com/danielberkompas/cloak_ecto/blob/master/lib/cloak_ecto/migrator.ex#L72
Likely this could be resolved by adding function heads for these cases, like already exist for embeds.
I want to create a unique constraint on an encrypted column, but since it's :binary
(which gets stored as bytea
in Postgres), the unique index doesn't work as expected. I followed the _hash
instructions, but that value is stored as :binary
too, so has the same limitation. Am I'm missing something or is this functionality not supported? Is there a way to store the value as a base64 string instead of binary?
The Cloak.Ecto.Type
module does not conform to the Ecto.Type
behavior. For example, equal/3
is not implemented.
Because of this, following the cloak_ecto
usage instructions results in a custom Ecto type which does not conform to the Ecto.Type
behavior. This can result in UndefinedFunctionError
instances when Ecto tries to use the custom type in a way that is not implemented:
** (UndefinedFunctionError) function Application.Encrypted.Binary.equal?/2 is undefined or private
I believe either Cloak.Ecto.Type
should be updated to use Ecto.Type
such that the default implementations can be used as fallbacks, or the usage instructions should be updated to let consumers of use Cloak.Ecto.Binary
know that they either need to use Ecto.Type
or define the missing methods.
I am getting this error message at compile time:
module Cloak.Ecto.PBKDF2 is not loaded and could not be found
Using Erlang/OTP 24 [erts-12.2]
and {:pbkdf2, "~> 2.0", github: "miniclip/erlang-pbkdf2"}
in mix.exs
.
How to solve this issue?
Hi there. First off, thanks so much for the amazing library! It's been extremely useful for our business and maintaining security compliance.
I was looking at the documentation for the mix cloak.migrate.ecto
task: https://hexdocs.pm/cloak_ecto/rotate_keys.html#content
In the example, the docs say to simply change the labels for your keys and add a new :default
entry:
Then change the :default label to the new key, and demote the existing key to the :retired label.
config :my_app, MyApp.Vault,
ciphers: [
default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V2", key: <<...>>},
retired: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: <<...>>}
]
This example works as expected. However, the documentation is a bit misleading. It seems that cloak
ignores labels like :default
and :retired
. Instead, it relies on the order of the cipher entries.
For example, consider this configuration:
config :my_app, MyApp.Vault,
ciphers: [
retired: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: <<...>>},
default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V2", key: <<...>>}
]
The only difference is the order of the entries. When we run mix cloak.migrate.ecto
, the keys aren't rotated and remain V1. It seems the migration picks the first cipher entry.
Happy to provide more information if necessary. Thanks!
Hello all!
I'm sorry if the question was already araised (I didn't find anything), but was is the reason why Cloak migrator doesn't support custom Ecto embeds?
https://github.com/danielberkompas/cloak_ecto/blob/master/lib/cloak_ecto/migrator.ex
defp cloak_field?({_field, {:embed, %Ecto.Embedded{}}}) do
false
end
defp cloak_field?({_field, {:parameterized, Ecto.Embedded, %Ecto.Embedded{}}}) do
false
end
I'm asking since I'm going to have something like this:
defmodule Vault do
use Cloak.Vault,
otp_app: :app
end
defmodule Vault.Value do
use Cloak.Ecto.Binary,
vault: Vault
def embed_as(_format), do: :dump
def dump(nil), do: {:ok, nil}
def dump(value) do
with {:ok, encrypted} <- super(value) do
{:ok, Base.encode64(encrypted)}
end
end
def load(nil), do: {:ok, nil}
def load(value), do: super(Base.decode64!(value))
end
defmodule Partner do
schema "partners" do
field(:code, :string)
embeds_one(:credentials, PartnerCredentials, on_replace: :update)
end
defmodule PartnerCredentials do
@primary_key false
embedded_schema do
field(:client_id, :string)
field(:client_secret, Vault.Value)
field(:refresh_token, Vault.Value)
end
end
So my client_secret
and refresh_token
get serialized/deserialized without problems, but I can't apply migration in case of rotating keys - credentials
field will be just skipped.
Were there any problems with embeds so you don't have support of them?
Any response from you would be appreciated. Thanks!
When using cloak_ecto in a typed Ecto Schema (see https://hexdocs.pm/typed_ecto_schema/), Dialyxir (see https://hex.pm/packages/dialyxir) halts with an unknown type error:
$ mix dialyzer
[…]
lib/db/contact.ex:0:unknown_type
Unknown type: Cloak.Ecto.SHA256.t/0.
________________________________________________________________________________
lib/db/contact.ex:0:unknown_type
Unknown type: DB.Encrypted.Binary.t/0.
Ecto Schema:
defmodule DB.Contact do
typed_schema "contact" do
field(:email, DB.Encrypted.Binary)
end
end
defmodule DB.Encrypted.Binary do
use Cloak.Ecto.Binary, vault: Transport.Vault
end
See https://github.com/etalab/transport-site/blob/master/apps/transport/lib/db/contact.ex, to reproduce the problem remove the line in .dialyzer_ignore.exs
.
i use Decimal for sensitive numbers and cloak_ecto doesn't seem to support it.
it doesn't look too hard to implement, but can someone check this? seems to be working for me.
defmodule Cloak.Ecto.Decimal do
defmacro __using__(opts) do
opts = Keyword.merge(opts, vault: Keyword.fetch!(opts, :vault))
quote do
alias Decimal, as: D
use Cloak.Ecto.Type, unquote(opts)
def cast(value), do: Ecto.Type.cast(:decimal, value)
def before_encrypt(value) do
case Ecto.Type.cast(:decimal, value) do
{:ok, d} -> D.to_string(d)
_error -> :error
end
end
def after_decrypt(value) do
case D.new(value) do
%D{} = d -> d
_error -> :error
end
end
end
end
end
Hi! This is an awesome library, I've integrated it into apps several times before without issues.
I'm in the middle of writing a blog post for how to integrate cloak
and cloak_ecto
into the Phoenix auth generators, to encrypt the user's email while data is at rest. In my previous attempts, I've always used PostgreSQL as my database. But recently, I've been using SQLite3 more since it is small, lightweight and easy to get set up.
However, I'm wondering if the library does not support the SQLite database adapter for some reason, or if I am not doing something obvious here - I'm getting the following error when trying to insert fields.
[error] GenServer #PID<0.610.0> terminating
** (Ecto.ChangeError) value `"[email protected]"` for `SecureApp.Accounts.User.email` in `insert` does not match type SecureApp.Encrypted.Binary
(ecto 3.9.4) lib/ecto/repo/schema.ex:1010: Ecto.Repo.Schema.dump_field!/6
(ecto 3.9.4) lib/ecto/repo/schema.ex:1019: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
(stdlib 4.1.1) maps.erl:411: :maps.fold_1/3
(ecto 3.9.4) lib/ecto/repo/schema.ex:1017: Ecto.Repo.Schema.dump_fields!/5
(ecto 3.9.4) lib/ecto/repo/schema.ex:951: Ecto.Repo.Schema.dump_changes!/7
(ecto 3.9.4) lib/ecto/repo/schema.ex:359: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(secure_app 0.1.0) lib/secure_app_web/live/user_registration_live.ex:54: SecureAppWeb.UserRegistrationLive.handle_event/3
I've set up and started the Vault
module, as well as integrated the Binary
and HMAC
types:
defmodule SecureApp.Vault do
use Cloak.Vault, otp_app: :secure_app
end
defmodule SecureApp.Encrypted.Binary do
use Cloak.Ecto.Binary, vault: SecureApp.Vault
end
defmodule SecureApp.Hashed.HMAC do
use Cloak.Ecto.HMAC, otp_app: :secure_app
end
The schema is as following:
schema "users" do
field :email, SecureApp.Encrypted.Binary
field :email_hashed, SecureApp.Hashed.HMAC
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
timestamps()
end
I haven't gotten a chance to switch the database adapter over to Postgres yet, but that will be my next debugging step. Just wondering if I am missing something here?
The code (WIP) for the blog post is public, you can find it here. It is a very minimal working example of the error I am seeing.
Thanks!
I realize the readme calls out the ability to use cloak_ecto to create hashed searchable columns but it would be nice to offer more of a first-class option for this similar to this ruby lib https://github.com/ankane/blind_index
They reference this article which breaks down what this would look like https://www.sitepoint.com/how-to-search-on-securely-encrypted-database-fields
It's common to need to do migrations in the database and convert data between encrypted fields with the following conditions:
Unfortunately this process is not documented and trying it myself resulted in the same error as danielberkompas/cloak#125 .
TLDR encrypt_existing_data.md solution is suboptimal.
I want to change a DateTime field to Date.
DateTime.to_date()
I did manage to read through a bunch of cloak's code and come up with a solution.
MyApp.Vault.start_link()
to start the Vault in Release (solves danielberkompas/cloak#125).load()
to decrypt your data.dump()
to encrypt data.Here's my full(ish) code, because I hate it when I find a solution online and a crucial piece is left out.
defmodule MyApp.Repo.Migrations.AlterTransactionDatetime do
use Ecto.Migration
import Ecto.Query, warn: false
alias MyApp.Repo
alias MyApp.Encryption.Types
def up do
dates =
from(t in "table", select: {t.id, t.datetime})
|> Repo.all()
|> Enum.map(fn {id, datetime_encrypted} ->
{:ok, datetime} = Types.DateTime.load(datetime_encrypted)
{:ok, new_val} = datetime |> DateTime.to_date() |> Types.Date.dump()
{id, new_val}
end)
alter table(:table) do
remove :datetime
add :date, :binary
end
flush()
for {id, date} <- dates do
from(t in "table", where: t.id == ^id, update: [set: [date: ^date]])
|> Repo.update_all([])
end
end
end
defmodule MyApp.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :my_app
def migrate do
MyApp.Vault.start_link()
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
[...]
end
First of all thank you for the recent release!
I've noticed some of my tests are failing due to:
cloak_ecto/lib/cloak_ecto/types/sha_256.ex
Lines 78 to 86 in 05ea920
I have a schema similar to
schema "tokens" do
field(:token_hash, Cloak.Ecto.SHA256)
...
end
I can create a new changeset from this schema, however the initial value will be nil
for :token_hash
.
If I then call the following: put_change(changeset, :token_hash, "my_token")
the String.valid?(nil)
in hash_string(string)
will fail.
I'm not entirely sure if a check needs to be added in the hash_string
function to make sure first the value are not nil
, or if I need to make sure first the changeset contains a valid string for the hash first.
One way I can resolve this is to add a default empty string value to field(:token_hash, Cloak.Ecto.SHA256, default: "")
.
Let me know if this is a non issue and feel free to close this.
Thanks
When you view the hex docs the link to see the source code of a function (eg here) the link goes to the main
branch: https://github.com/danielberkompas/cloak_ecto/blob/main/lib/cloak_ecto/types/binary.ex while this repo uses master
as main branch: https://github.com/danielberkompas/cloak_ecto/blob/master/lib/cloak_ecto/types/binary.ex.
Just attempted a bump to elixir 1.12 + Erlang 24 and getting a bunch of these:
apps/web/test/web/my_test.exs:22
** (UndefinedFunctionError) function :crypto.block_encrypt/4 is undefined or private, use crypto:crypto_one_time/5, crypto:crypto_one_time_aead/6,7 or crypto:crypto_(dyn_iv)?_init + crypto:crypto_(dyn_iv)?_update + crypto:crypto_final instead
stacktrace:
(crypto 5.0.1) :crypto.block_encrypt(:aes_gcm, <<195, 225, 165, 141, 41, 78, 62, 205, 98, 180, 191, 110, 234, 172, 72, 184, 62, 87, 136, 164, 190, 51, 242, 243, 163, 183, 119, 33, 180, 102, 137, 119>>, <<120, 219, 37, 247, 92, 246, 21, 28, 46, 15, 17, 201, 227, 63, 247, 245>>, {"AES256GCM", "secret"})
(cloak 1.0.3) lib/cloak/ciphers/aes_gcm.ex:46: Cloak.Ciphers.AES.GCM.encrypt/2
(web 0.1.0) lib/cloak_ecto/type.ex:37: Web.Encrypted.Binary.dump/1
(ecto 3.6.1) lib/ecto/type.ex:914: Ecto.Type.process_dumpers/3
(ecto 3.6.1) lib/ecto/repo/schema.ex:953: Ecto.Repo.Schema.dump_field!/6
(ecto 3.6.1) lib/ecto/repo/schema.ex:966: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
(stdlib 3.15) maps.erl:410: :maps.fold_1/3
(ecto 3.6.1) lib/ecto/repo/schema.ex:964: Ecto.Repo.Schema.dump_fields!/5
(ecto 3.6.1) lib/ecto/repo/schema.ex:897: Ecto.Repo.Schema.dump_changes!/6
(ecto 3.6.1) lib/ecto/repo/schema.ex:335: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(ecto 3.6.1) lib/ecto/repo/schema.ex:942: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
(ecto_sql 3.6.1) lib/ecto/adapters/sql.ex:1005: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
(db_connection 2.4.0) lib/db_connection.ex:1512: DBConnection.run_transaction/4
Thanks for the great project!
I’m not quite sure where the various equal?/2
implementations are used (on plain or encrypted values), but given what cloak_ecto
is used for, should they not perhaps be constant time comparisons?
I've set the Ecto.Type to Encrypted.Binary
for my context, but I get an error.
I've verified Cloak setup and installation by Vault.encrypt("plaintext")
before setting up the struct.
Here is the error:
** (ArgumentError) invalid or unknown type MaitreD.Encrypted.Binary for field :password
lib/ecto/schema.ex:2025: Ecto.Schema.check_field_type!/3
lib/ecto/schema.ex:1732: Ecto.Schema.__field__/4
lib/maitre_d/grubhub_cred.ex:7: (module)
(stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
(elixir) lib/kernel/parallel_compiler.ex:229: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
Is there a step missing from the documentation to expose the EctoType
s?
I have a schema that was created with a migration for ecto_ch:
def change do
create table(:settings, primary_key: false, engine: "MergeTree") do
# The primary key for this is the organization id that the settings belong to.
add(:organization_id, :uuid, primary_key: true)
add(:token, :binary)
timestamps(type: :utc_datetime)
end
end
with a model like so:
defmodule MyApp.Orgs.Settings do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
schema "settings" do
field(:token, MyApp.Orgs.SecretKey)
timestamps(type: :utc_datetime)
belongs_to(:organization, MyApp.Orgs.Organization,
references: :id,
type: Ecto.UUID
)
end
@doc false
def changeset(settings, attrs) do
settings
|> cast(attrs, [:token, :organization_id])
|> validate_required([:token, :organization_id])
end
end
where SecretKey is:
defmodule MyApp.Orgs.SecretKey do
use Cloak.Ecto.Binary, vault: MyApp.Orgs.Vault
end
defmodule MyApp.Orgs.Vault do
use Cloak.Vault, otp_app: :myapp
end
I have a simple test that makes a settings object and then lists the object. but I'm getting this error:
Assertion with == failed
code: assert Orgs.list_settings() == [settings]
left: [
%myapp.Orgs.Settings{
__meta__: #Ecto.Schema.Metadata<:loaded, "settings">,
inserted_at: ~U[2024-06-12 20:10:48Z],
token: :error,
organization: %myapp.Orgs.Organization{
__meta__: #Ecto.Schema.Metadata<:loaded, "organizations">,
id: "9a31980f-0342-49af-8377-8e8ed9fc2852",
inserted_at: ~U[2024-06-12 20:10:48Z],
name: "some name",
settings: #Ecto.Association.NotLoaded<association :settings is not loaded>,
updated_at: ~U[2024-06-12 20:10:48Z],
users: #Ecto.Association.NotLoaded<association :users is not loaded>
},
organization_id: "9a31980f-0342-49af-8377-8e8ed9fc2852",
updated_at: ~U[2024-06-12 20:10:48Z]
}
]
right: [
%myapp.Orgs.Settings{
__meta__: #Ecto.Schema.Metadata<:loaded, "settings">,
inserted_at: ~U[2024-06-12 20:10:48Z],
token: "some token",
organization: %myapp.Orgs.Organization{
__meta__: #Ecto.Schema.Metadata<:loaded, "organizations">,
id: "9a31980f-0342-49af-8377-8e8ed9fc2852",
inserted_at: ~U[2024-06-12 20:10:48Z],
name: "some name",
settings: #Ecto.Association.NotLoaded<association :settings is not loaded>,
updated_at: ~U[2024-06-12 20:10:48Z],
users: #Ecto.Association.NotLoaded<association :users is not loaded>
},
organization_id: "9a31980f-0342-49af-8377-8e8ed9fc2852",
updated_at: ~U[2024-06-12 20:10:48Z]
}
]
stacktrace:
test/myapp/orgs_test.exs:15: (test)
Which implies that when I go to decrypt from my database, it's unable to use the vault to decrypt my encrypted key. I updated my test_helper.exs to ensure the vault was started, but no dice. MyApp.Orgs.Vault.start_link()
Hi all,
I presume I can't order by in Pg as the data is encrypted and Pg doesn't know what it is?
Thanks.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.