paragonie / paseto Goto Github PK
View Code? Open in Web Editor NEWPlatform-Agnostic Security Tokens
Home Page: https://paseto.io
License: Other
Platform-Agnostic Security Tokens
Home Page: https://paseto.io
License: Other
I really like this initiative, so we can get rid of pesky JWT.
Although I probably don't understand all the dangers, I know there are some, which make me uncertain even if I were to do JWT seemingly done right :/
And although I really like the name itself, the usability of the acronym is really bad. It's more or less impossible to search for. Is there any chance of an acronym change?
I don't have any good suggestions off the top of my head, but I'm willing to put some effort into it if you're open to an acronym change of some sort?
Thanks for this project. I have been reading through trying to get a grasp on things, and I think an important function for your API would be generating keys in a secure matter.
I see from your tests that symmetric keys for v2 are generated from 32 random bytes. Is 256 bits considered secure enough here? Could this be documented somewhere?
I think it would be useful if ProtocolInteface
had two extra methods that would correspond to the algorithms in use:
generateAsymmetricKeys()
: generates asymmetric keysgenerateSymmetricKey()
: generates a symmetric keyIt would be up to each version to decide what is considered a secure enough key. These could take an optional key length, preferably with guidance in the documentation as to which length to choose?
I couldn't find any information about what is the expected order of keys in the payload, it would be useful that in any implementation such code:
encode(decode(Payload)) == Payload
Currently from what I see there is nothing that ensures such behaviour. This could be achieved with #90 and usage of ASN.1 DER format for example, but right now standard requires payload to be correct Base64 encoded JSON token.
I'm trying to figure out if I should support key identifiers in pypaseto. I'm inclined to just say no, but I noticed a reference in this repo to what appears to be key ids:
paseto/tests/JsonTokenTest.php
Line 47 in ac3c4cc
But I also found this closed issue about it:
As far as I can tell, there is no built-in way to parse a footer in advance of decoding. I could provide a "peek at the footer" function that just base64 decodes the last segment, but I don't want people using the data in the footer unauthenticated. They could be required to write their own "peek" function, but I don't want to encourage that either (as they might be tempted to use it for things apart from key-selection).
I almost wonder if there is a benefit to telling people it is unsupported. For users who insist, they can reference their alternative keys out-of-band (query string, post parameter, extra junk they add to the end of the token, etc.). They could then load the key based on what was provided out-of-band, which is unauthenticated (as they can't authenticate without the key anyhow).
Any suggestions?
Hi,
I just finish to read your post about JWT security issues.
Now I need to plan to migrate my code from JWT to another solution.
I saw you made 3 Pre-release, but you set your version 0.3.0
as stable
.
So I don't know if you consider PAST production ready.
PAST isn't stable/secure enough to be use in production ?
Thanks for your work ;)
Since there's no issue open specifically for this:
Currently the documentation only goes so far as to specify the Paseto message format, but does not specify how to process this payload after decrypting or verifying it.
From reading the reference implementation I can probably answer most of the following myself, the goal here is to highlight questions that the spec should answer if processing the payload is going to be part of the specification, it's not just a series of personal questions :p
How payloads represented as strings should be encoded into bytes (utf8?)
Is a JSON encoded payload part of the spec? Is it required that (received||sent) payloads are in this format, or optional?
Specify the type (or even data) structure (if any) that the JSON payload should conform too
array<string, string>
, but I think it actually allows array<string, mixed>
due to a type hint diverging with the doc block. See also probable need for type validation on bulk setting of claims.Are arbitrary keys allowed to be set? (or is there a reserved character set for example?)
Are any keys reserved for certain purposes? (e.g. the ones used for rules like expiry)
Are these reservations case sensitive? (e.g. should an expiry key with incorrect case be ignored?)
Should users be allowed to set reserved keys like arbitrary other keys, or should distinct mechanisms be enforced?
If users can set these, do we fail if the user puts unexpected (e.g. unparseable) data in these reserved fields (i.e. should writes to reserved fields be validated)?
Are rules part of the spec? (if so what are they?)
Are rules specified as reserved keys, or are they in a structurally different part of the JSON payload to user data?
Is rule validation required? (if no, what should be the default?)
Do we fail open or closed if a rule is of an unexpected format?
Will payload processing receive spec updates (e.g. potential changes to rules, addition of rules), how should this be versioned?
If payload processing is versioned, where do we put this version?
Can probably ask at least the encoding question for processing the footer too, though that seems really intended just to be a string.
This is most likely a non issue but might be worth considering since one might think a greater version is always to be preferred.
Yo should make possible to use paseto on your website (like JWT does).
It would be great!
For paseto to become a viable JOSE alternative, it probably needs to cover more of the JOSE suite including JWK.
At DC26 CiPHPerCoder talked about how key objects in library implementations should be typed and not just byte arrays. To this end, paseto should have a JSON object format for keys to a) guide implementations of key classes and b) provide simple standard ways of serializing/deserializing keys.
There's an RFC 3339 specifying timestamps format for use in Internet protocols.
I think it's a good idea to:
1) explicitly state that "exp"
(if present) contents MUST follow that RFC.
2) fix the examples and test vectors to conform with that RFC. Currently the examples in README contain "exp": "2039-01-01T00:00:00"
, which doesn't have a timezone offset specified, in violation of RFC 3339. As a consequence, RFC-conformant parsers will refuse to parse this timestamp (saying from my personal experience, trying to implement PAST in Rust language).
In README a "Caution" quote explains one use case where PAST should not be used.
Can we have more details about the use cases?
For example by having two sections "When to use PAST", "When NOT to use PAST".
Hello,
I think for a readability of the project, it could be very helpfull to split the specification of paseto and the implementation in php. It allow people to see to evolution of the spec and the implemntation without any missmatch.
thanks
Some people might want to allow the token to possess some sort of identifier for the key used to encrypt it. If we decide to implement this, it's going to be:
In its current state, can paseto be used to simply sign arbitrary payloads ... e.g. generic JSON or any base64 encoded value? Looking for an alternative to JWS and came across paseto, but it seems to be primarily focused on claims and an alternative to JWTs.
Right now, the payload, as defined, seems to have only one key, exp
. It seems like, somewhere and somewhen, somebody is going to try to stick a colliding key into the payload. Also, down the line, it strikes me that there may be additional metadata to add to the token (which could cause forward-upgrade issues for users with colliding keys). Maybe it'd be better for the basic payload structure to be:
{
"exp": <expiry>,
"d": <any legal JSON token>
}
For ease of use this could be constrained such that d
must be a JSON object (in particular that'd probably make static-language implementations a little bit easier).
Thoughts?
I'm mostly just opening a ticket so people know what I'm already working on.
Can you see if Paseto can be used for OAuth2 to replace \OAuth2\ResponseType\JwtAccessToken
There was also desire to have customization bshaffer/oauth2-server-php#795 (comment)
Hello,
In the documentation we got example to create and validate token, but to avoid miss-usage it could be interesting to have a dummy example.
So, for me, I will do something like this :
init.php
<?php
use ParagonIE\Paseto\Keys\{
AsymmetricSecretKey,
SymmetricKey
};
$privateKey = new AsymmetricSecretKey(sodium_crypto_sign_keypair());
$publicKey = $privateKey->getPublicKey();
$sharedKey = new SymmetricKey(random_bytes(32));
// shared key is stored in a config file? a database? ....
getToken.php
<?php
use ParagonIE\Paseto\Builder;
use ParagonIE\Paseto\Purpose;
use ParagonIE\Paseto\Keys\SymmetricKey;
use ParagonIE\Paseto\Protocol\Version2;
// reuse our shared key
$token = Builder::getLocal($sharedKey, new Version2());
// the user is authenticated insight the app
$userId = $app->getTheUserId();
$token = (new Builder())
->setKey($sharedKey)
->setVersion(new Version2())
->setPurpose(Purpose::local())
->setExpiration(
(new DateTime())->add(new DateInterval('P1D'))
)
->setClaims([
'user_id' => $userId,
]);
echo $token;
// make the token available in the frontend.
bar.js
$.ajax({
method: 'POST',
url: 'foo.php',
data: { token: tokenPreviouslyGivenByTheServer, data: 'some-data-to-save }
})
.done(function( msg ) {
alert( "Data Saved: " + msg );
});
foo.php
<?php
use ParagonIE\Paseto\Exception\PasetoException;
use ParagonIE\Paseto\Keys\SymmetricKey;
use ParagonIE\Paseto\Parser;
use ParagonIE\Paseto\Purpose;
use ParagonIE\Paseto\Rules\{
IssuedBy,
NotExpired
};
use ParagonIE\Paseto\ProtocolCollection;
$providedToken = htmlspecialchars($_POST['token']);
/**
* @var string $providedToken
* @var SymmetricKey $sharedKey
*/
$parser = Parser::getLocal($sharedKey, ProtocolCollection::v2());
// This is the same as:
$parser = (new Parser())
->setKey($sharedKey)
// Adding rules to be checked against the token
->addRule(new NotExpired)
->addRule(new IssuedBy('issuer defined during creation'))
->setPurpose(Purpose::local())
// Only allow version 2
->setAllowedVersions(ProtocolCollection::v2());
try {
$token = $parser->parse($providedToken);
} catch (PasetoException $ex) {
/* Handle invalid token cases here. */
header('HTTP/1.0 401 Unauthenticated');
}
if (!($token instanceof \ParagonIE\Paseto\JsonToken))
{
header('HTTP/1.0 401 Unauthenticated');
}
$userId = $token->get('user_id');
// ....
// do something with the posted data
Is this something that could be considered as valid? In the other case, how should we handle it? An example is always a starting point
thanks
Hello, I'd like to write few implementations in node and java but find the documentation fairly inadequate.
Is there a well written specification document available?
There is an inconsistency between the test vector specification in the RFC and reference implementation for test vectors 2-E-5, 2-E-6 and 2-S-2.
For example, for 2-E-5, published rfc draft and rfc source specify footer {"kid":"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo"}
while
reference implementation actually uses footer with kid zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN
.
The same discrepancy exists for 2-E-6.
For 2-S-2, published rfc draft and rfc source use kid dYkISylxQeecEcHELfzF88UZrwbLolNiCdpzUHGw9Uqn
while reference implementation uses kid zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN
.
Please confirm, is it safe to assume that reference implementation contains the correct test vector and rfc source should be amended?
Edit: added 2-S-2
For example in
paseto/src/Keys/AsymmetricSecretKey.php
Lines 36 to 44 in 1a40019
The given protocol version is used to determine conditions for key construction. However the protocol version of a key is not checked when using the key directly with a protocol, e.g. in
paseto/src/Protocol/Version2.php
Lines 105 to 121 in 1a40019
Which means that the conditions checked in the key's constructor cannot be guaranteed (since version 1 AsymmetricSecretKey key may be constructed and passed to version 2 as a key for example).
This I suppose leads to the more general question: if keys are dependent on a version, should there be distinct types for these different versions?
At the very least I think that the protocols should be checking the version associated with any keys they receive?
The term public key as used in the token specification is slightly confusing, I'd suggest renaming it to token key to better reflect it's usage.
eg
version.purpose.tokenkey.ciphertext
...preferably, all of the above.
See #67 for the pull request.
Languages desired:
After discussing with @aidantwoods in a separate issue (aidantwoods/swift-paseto#5), it looks like PAE assumes that deterministic encryption is available in XChaCha20-Poly1305 encryption.
Even if specifying nonces has been made optional in paseto-rfc-01
, it is still assumed that the same random bytes will be used in PAE and in the subsequent encryption/decryption:
2. Generate 24 random bytes from the OS's CSPRNG.
3. Optionally, calculate [...] This will be our nonce, "n".
* If this step is omitted, the output of step 2 is "n" instead.
4. Pack "h", "n", and "f" together (in that order) using PAE (see
Section 2.2). We'll call this "preAuth".
This is problematic because @jedisct1 plans to remove specifying nonces from the public libsodium APIs (jedisct1/swift-sodium#173 (comment)).
So I thought I'd add an example from the readme to my test suite, and it turns out there are some typos (unless of course my implementation is somehow completing words and changing a year ;p)
I think "exp":
should be "expires":
and the year should be 2019
instead of 2039
.
I would have opened this as a PR, but I wanted to check whether the second example also has the same problem. It appears to, but I can't conclude this because there isn't a public key available to verify the message ;-) (this is a request to add the public key to the readme)
https://github.com/paragonie/past/blob/89f9b32b73d9c45d2ddb3ff4dd000dddfd2c5777/src/Parser.php#L230 => Should be setAllowedVersions()
.
I noticed that paseto.io is registered and currently just points to chronicle. I'm not sure if there are any plans yet. I think some of JWT's usage/success is driven from the utility and visibility of that marketing-style site.
Some ideas:
Hello! I'm building this example
require __DIR__ . '/vendor/autoload.php';
use ParagonIE\Paseto\Keys\{
AsymmetricSecretKey,
SymmetricKey
};
$privateKey = new AsymmetricSecretKey(sodium_crypto_sign_keypair());
$publicKey = $privateKey->getPublicKey();
$sharedKey = new SymmetricKey(random_bytes(32));
When I ran this code in Eclipse, it stay very slow ! Do I need to use other way for it?
My last test, the code spent 10 minutes to finish! What could be happening?
Thank you!
The first draft (current as of v0.2 of this repository) defined four purposes:
auth
)enc
)sign
)seal
)However, seal
has since been removed due to a lack of a good real-world use case.
I've been discussing this with other crypto/security experts, and I think we might be able to get the same utility out of PAST if we dropped auth
, and changed our paradigm to look like this:
local
)public
)If you need the equivalent use-case of v2.auth
(e.g. so you can inspect some data in the token before decoding/verifying it normally), you can simply encrypt an empty string and append your public data in the footer.
This would simplify our design, reduce our technical debt, and minimize the attack surface to a reasonable level. However, this is also a secure-by-default feature: If you're just storing data in a token and using the user as a data mule for local usage, it won't be plaintext.
Would be great to have an official PASETO logo. I know from a technical standpoint PASETO speaks for itself, but for whatever reason having a logo does seem to give projects a greater sense of credibility. I think something as simple as that would help to grow the community.
Error happens on macOs when trying to generate the token
ReferenceError: cb is not defined
at encoder.encrypt (/Users/.../server/src/utils.js:556:16)
at /Users/.../server/node_modules/paseto.js/lib/utils.js:239:39
at sodium.ready.then (/Users/.../server/node_modules/paseto.js/lib/protocol/V2.js:146:14)
Edit:
This happens when i try to use something that is not the callback function
Error: Failed to construct 'TextDecoder': the 'fatal' option is unsupported.
I will move this to the Paseto.js
In the 'public'
case of:
Lines 190 to 220 in c44d334
$this->key
is checked to be an instance of AsymmetricPublicKey
, which is later passed to JsonToken::setKey
. However, this requires the given key to be an instance of AsymmetricSecretKey
in the 'public'
case.
Lines 374 to 382 in c44d334
Obviously there's going to be a type mis-match here, but on a more general note:
Given that the JsonToken
is constructed with a key for signing, I don't think it would ever make sense to have the parser produce a JsonToken
in the public case, because it would require knowledge of the secret key to sign, but the parser is only really concerned with verification which requires only the public key (which is likely all the receiver of a signed object has?).
I think perhaps the key/signing should be decoupled from the JsonToken
so that the parser can produce these objects without requiring a key, and the key should be provided when signing takes place (perhaps in a new SignableJsonToken
class for example that is constructed using a JsonToken
and a key).
Hello
This is a very nice project, and I'm considering to switch from JWT.
I know JWT also doesn't have this feature, But I want to know paseto project can support "set token invalid before expired" feature now? or in the future?
thx.
Line 139 in ed7c455
This tests catches the PHPUnit\Framework\AssertionFailedError
. Is that actually intended? It seems the test is a bit pointless now?
Code in this repository: https://github.com/conferencetools/auth-module/blob/master/src/Auth/Extractor/PasetoCookie.php results in an empty cookie being set on my production env; works fine in dev.
PHP environment is
FROM php:7.2-fpm-alpine3.7 as php
RUN docker-php-ext-install pdo_mysql
Fairly sure it's an environment thing, but I'd expect an exception instead of an empty string.
Interesting addendum: switched to V1 and it works fine. Suspect it might be something alpine/libsoduim related.
I should make it explicit that neither PAST nor JWT are good solutions for some of the problems that developers try to use them for.
References:
I became interested in your paseto standard after feeling a bit uneasy about JWT's, but I have a few questions and wasn't sure how else to ask but to make a github issue (feel free to add the answers to your documentation).
Is the body and its claims always encrypted? If not, when it is it not encrypted?
Is the footer always unencrypted?
Is the footer signed?
If you want the website frontend to see certain claims or data, would you advise putting that in a json objected in the footer, instead of into the body?
Is the footer validated against anything? Or is it just a grab bag of whatever I want to put in it, including nothing?
Which claims are "built into the standard" and are expected to be validated or read by the implementing libraries?
Should Paseto be used for sending session id's and non-confidential data about the user to the website frontend, to be stored in secure cookies? (ie: what most people use JWT's for)
There are two instances of 'expires'
in the Version2VectorTest
tests:
paseto/tests/Version2VectorTest.php
Line 226 in fa662c6
paseto/tests/Version2VectorTest.php
Line 286 in fa662c6
Everything else uses 'exp'
. Should these be altered for consistency?
Like the English word "pasta" without the final "a". It rhymes with "frost" and the first syllable in "roster".
I don't know about you, but in Philly, we pronounce frost as frauwst
:P
Three of the purposes defined thus far are:
auth
, which is authenticated with a shared secret keyenc
, which is encrypted with a shared secret key, but uses authenticated encryptionsign
, which is authenticated with a secret key and verified with a public keyBut seal
is a bit of an oddball. It implements what libsodium refers to as "anonymous public-key encryption". The ciphertext has integrity assurance (via AEAD), but the sender is anonymous.
Do we want anonymous public-key encryption in PAST? Is there a valid real-world use-case for it?
I suspect the answer is "no", and will be proposing a replacement that uses authenticated public-key encryption. The interface will, instead, look something like this:
public function asymmetricEncrypt(
string $message,
AsymmetricPublicKey $recipientPublicKey,
AsymmetricSecretKey $mySecretKey,
string $footer = ''
): string;
public function asymmetricDecrypt(
string $token,
AsymmetricPublicKey $senderSecretKey,
AsymmetricSecretKey $mySecretKey,
string $footer = ''
): string;
Details:
Hello, I'm wondering if my system is set up the wrong way or if this library is supposed to force the use of sodium_compat? As far as I can tell from some xdebugging and analysis with Blackfire, when executingVersion2::aeadDecrypt()
(here), there is never an attempt to call sodium_crypto_aead_xchacha20poly1305_ietf_decrypt
directly, but \ParagonIE_Sodium_Compat::crypto_aead_xchacha20poly1305_ietf_decrypt
is used. Modifying that line of code to use the sodium function cuts the cost from 25 ms to 4 ms, with most of that being CPU time.
Am I doing something wrong, or is this an oversight? The sodium_compat documentation seems to specify that calling the library directly with ParagonIE_Sodium_Compat::
is for implementations targetting version below PHP 5.3, but as this library targets PHP 7.0, I can't see how that is relevant.
Thanks!
Regarding the spec definition of LE64
:
LE64()
encodes a 64-bit unsigned integer into a little-endian binary string.
When this is used with PAE
, LE64
is exclusively given lengths (of an array and of a byte string)
function PAE(pieces) {
if (!Array.isArray(pieces)) {
throw TypeError('Expected an array.');
}
var count = pieces.length;
var output = LE64(count);
for (var i = 0; i < count; i++) {
output += LE64(pieces[i].length);
output += pieces[i];
}
return output;
}
However, some languages (e.g. PHP, Swift, ...) give signed integer results for these lengths: reducing the effective number of bytes that LE64
will ever act on to 63 (even after conversion to a UInt64 if possible).
Given the goal of platform agnosticism I think the spec should define what to do in the case that the lengths used by PAE
cannot be more than 63 (effective) bytes in the implemented language. (Indeed the reference PHP implementation has this restriction). Perhaps even given that languages with this restriction exist, implementations in languages that do give unsigned 64 bit lengths should instead replicate the behaviour, e.g. be instructed to throw if the most significant bit is ever populated (i.e. the result is greater than something like UInt64.max/2
).
(actually hitting these numbers isn't at all likely to happen in practice of course by any sensible interpretation of the word, this mainly being a pedantic clarification on what the "right" (defined) thing to do is)
Following along with the usage documentation here: https://github.com/paragonie/past/tree/master/docs/02-PHP-Library, I've ran into the following exception when executing the first block of code:
PHP Fatal error: Uncaught Exception: Secret keys must be 32 or 64 bytes long; 96 given. in vendor/paragonie/past/src/Keys/AsymmetricSecretKey.php:40
Stack trace:
#0 encrypt.php(11): ParagonIE\PAST\Keys\AsymmetricSecretKey->__construct('\x84\x9E\xAC\xFB6t^\xBD\xEC\xC3Hq\xFA\xAD\x06...', 'v2')
#1 {main}
thrown in vendor/paragonie/past/src/Keys/AsymmetricSecretKey.php on line 40
This is the code I'm using:
<?php
require('vendor/autoload.php');
use ParagonIE\PAST\Keys\{
AsymmetricSecretKey,
SymmetricKey
};
$privateKey = new AsymmetricSecretKey(sodium_crypto_sign_keypair());
$publicKey = $privateKey->getPublicKey();
$sharedKey = new SymmetricKey(random_bytes(32));
I'm running this on macOS with PHP 7.1. I ran it with both libsodium and gmp installed and uninstalled, but got the same exception.
Did anything change in the implementation and is the documentation not up-to-date, or is there something else going on?
I found some test names that have been re-used, probably in error:
There are two 'Test Vector S-6'
entries in the Vector2VectorTest tests file. Should the second, which adds a non-empty footer to the first test, be renamed to S-7?
See:
paseto/tests/Version2VectorTest.php
Lines 286 to 297 in fa662c6
There is a signing test that re-uses a name from an encryption test. It should probably be renamed.
See:
paseto/tests/Version2VectorTest.php
Lines 299 to 305 in fa662c6
This is probably a copy-and-paste error when the corrensponding encryption test was re-used. I think it should be named S-8 instead.
https://www.ietf.org/mail-archive/web/cfrg/current/msg09612.html
Our reference implementation does the safe thing here, but we should, at minimum, document the use of typed objects rather than opaque strings to prevent local/public confusion attacks.
From Thai Duong:
In the case of an RNG failure (i.e. all zero bytes), the BLAKE2b of the nonce and message leaks the BLAKE2b hash of the message, which might be enough information (along with the plaintext length) to allow an attacker to recover a plaintext without knowing the key.
The story is similar for v1 with HMAC-SHA384.
Proposed solution: Instead of hashing the message and random nonce together, also include the secret key in the calculation. This changes the attack calculus from "find the input to a hash function with a known output" to "break a keyed PRF without knowing the key".
This solution is backwards compatible with what v1
and v2
are already doing (since the output is just a plaintext nonce that gets stored in the token). However, to make this behavior change explicit and easy to reason about, I'm thinking about incorporating these changes into a v3
and v4
.
Using the php example code at https://github.com/paragonie/past/tree/master/docs/02-PHP-Library, setClaims overwrites what was defined with setExpiration
$token = (new JsonToken())
->setKey($sharedKey)
->setVersion(Version2::HEADER)
->setPurpose('local')
->setExpiration((new DateTime())->add(new DateInterval('P01D')))
->setClaims(['example' => 'Hello world', 'security' => 'Now as easy as PIE']);
For an expiration to be set correctly, setClaims needs to come before setExpiration.
$token = (new JsonToken())
->setKey($sharedKey)
->setVersion(Version2::HEADER)
->setPurpose('local')
->setClaims(['example' => 'Hello world', 'security' => 'Now as easy as PIE'])
->setExpiration((new DateTime())->add(new DateInterval('P01D')));
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.