├── talk.txt ├── .gitignore ├── index.js ├── .jshintrc ├── test ├── story.txt ├── basic.js └── story.js ├── lib ├── block.js ├── history.js ├── wallet.js └── coin.js ├── package.json ├── README.md └── signflow.txt /talk.txt: -------------------------------------------------------------------------------- 1 | moved to Notes 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.Wallet = require("./lib/wallet"); 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "asi" : true, 5 | "esversion": 6 6 | } 7 | -------------------------------------------------------------------------------- /test/story.txt: -------------------------------------------------------------------------------- 1 | Generate key for Chris and save it locally. 2 | 3 | Chris mints a new coin, including encrypted content. 4 | 5 | 6 | 7 | 8 | 9 | Chris creates a payload, signs it, and encrypts it for Amy, writes out a history with Amy as the holder. 10 | -------------------------------------------------------------------------------- /lib/block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const jose = require("node-jose"); 3 | 4 | class Block { 5 | constructor(content, pubkey) { 6 | this.content = content; 7 | this.pubkey = pubkey; 8 | } 9 | signingContent() { 10 | return this.content + this.pubkey; 11 | } 12 | } 13 | 14 | module.exports = Block; 15 | -------------------------------------------------------------------------------- /lib/history.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | holder.give(doc, recipient) 4 | - acquires keys 5 | - calls subroutines to perform transformations 6 | 7 | content.reencrypt(holder, doc.content, recipient) 8 | - decryption 9 | - encryption 10 | 11 | 12 | 13 | history.give(holder, doc.history, recipient) 14 | - holder signs assertion giving to recipient 15 | - places in tree structure 16 | 17 | history.verify(doc.history) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "document-coin", 3 | "version": "0.0.1", 4 | "description": "Tiny blockchains for a big idea.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ansi-codes": "0.0.1-security", 8 | "babel-register": "^6.5.2", 9 | "node-jose": "^0.11.0", 10 | "prova": "^3.0.0", 11 | "tape": "^4.9.0", 12 | "urlsafe-base64": "^1.0.0" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "test": "node test/story.js -b" 17 | }, 18 | "author": "Chris Anderson", 19 | "license": "Apache-2.0" 20 | } 21 | -------------------------------------------------------------------------------- /lib/wallet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const jose = require("node-jose"); 3 | const Coin = require("./coin"); 4 | 5 | class Wallet { 6 | constructor(name) { 7 | this.name = name; 8 | } 9 | setupKeys() { 10 | this.keystore = jose.JWK.createKeyStore(); 11 | return this.keystore.generate("EC", "P-256").then((key) => { 12 | this.signingKey = key; 13 | }) 14 | } 15 | mint(content) { 16 | const mintedCoin = new Coin (); 17 | return mintedCoin.mint(content, this); 18 | } 19 | getName() { 20 | return this.name; 21 | } 22 | } 23 | 24 | module.exports = Wallet; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Crypto Currency for the Gift Economy 2 | 3 | This is an experimental project with more unknown unknowns than you can shake a stick at. Do not use it to run your business, unless your business is experimental JavaScript crypto "currency" games. 4 | 5 | Document Coin implements a signature-based data provenance tracking system. Restrictions on how viral a given coin can be are configured per-coin by the person who mints the coin. 6 | 7 | To run the test suite: 8 | 9 | ``` 10 | npm install 11 | npm start 12 | ``` 13 | 14 | ### Help wanted. 15 | 16 | If you want to get involved, opening issues is a great way to start. You can also reach out to me on twitter @jchris 17 | 18 | ### Next steps 19 | 20 | * Create a pubic API that works well for all kinds of data. 21 | * Connect to PouchDB 22 | * Make a demo game 23 | 24 | ## License 25 | 26 | Apache 2.0 27 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | // var jwt = require('webcrypto-jwt'); 3 | var jose = require("node-jose"); 4 | 5 | test("create a signing keypair and sign/verify an input", function (t) { 6 | t.plan(4) 7 | var keystore = jose.JWK.createKeyStore(); 8 | 9 | var props = { 10 | kid: 'gBdaS-G8RLax2qgObTD94w', 11 | alg: 'A256GCM', 12 | use: 'enc' 13 | }; 14 | keystore.generate("oct", 256). 15 | then(function(key) { 16 | t.equals(key.keystore, keystore, "the keystore") 17 | var input = "Hello Alice"; 18 | jose.JWS.createSign(key). 19 | update(input, "utf8"). 20 | final(). 21 | then(function(result) { 22 | console.log("result", result) 23 | t.equals(result.payload, "SGVsbG8gQWxpY2U") 24 | 25 | jose.JWS.createVerify(key). 26 | verify(result). 27 | then(function(decoded) { 28 | t.equals(decoded.payload.toString(), "Hello Alice") 29 | t.equals(decoded.key, key) 30 | // {result} is a Object with: 31 | // * header: the combined 'protected' and 'unprotected' header members 32 | // * payload: Buffer of the signed content 33 | // * signature: Buffer of the verified signature 34 | // * key: The key used to verify the signature 35 | }); 36 | 37 | }).catch(function (error) { 38 | t.error(error) 39 | }); 40 | }); 41 | }) 42 | -------------------------------------------------------------------------------- /signflow.txt: -------------------------------------------------------------------------------- 1 | Chris creates a payload, signs it, and encrypts it for Amy, writes out a history with Amy as the holder. 2 | 3 | encrypted payload 4 | - minter signature (immutable) 5 | public payload (immutable) 6 | - minter signature (immutable) 7 | 8 | minter 9 | - signing pubkey 10 | - encrypting pubkey 11 | - signature 12 | history 13 | [holder1, minter signature] 14 | 15 | 16 | replies with reply_to message id / signature? part of public content not history 17 | 18 | Amy decrypts the paylod to display it. 19 | 20 | Amy gives the coin to Irma by encrypting payload for Irma and, adding her to the holder list (tree?). 21 | 22 | combined history 23 | [holder2, holder1 signature] 24 | [holder1, minter signature] 25 | 26 | 27 | history-holders 28 | 29 | collection of jwt, run map to decode and view them by pubkey. 30 | 31 | 32 | JWT signed by giver : 33 | 34 | chrispubkey signed version of the givetree with amypubkey as holder. 35 | amypubkey signed version of the givetree with irmapubkey as holder. 36 | irma signs the givetree to sam. 37 | amy signs the givetree to isaac. 38 | 39 | Amy may or may not have learned about irma's give when she signs Isaacs, so we need to preserve that logical key structure when we sign, which means a copy of the tree per signature. And the client merges them all to view a coin's history. 40 | 41 | giver-depth is useful? 42 | to verify we verify all JWTs 43 | 44 | nested json web signature 45 | 46 | if we 47 | 48 | physical structure: 49 | 50 | JWT.content { 51 | holder : "Chris", 52 | giver : { // minter. the root is self signed 53 | signing-key : giver-signing-pubkey, 54 | encryption-key : giver-encryption-pubkey 55 | } 56 | recevier : "Amy pubkey" 57 | } .Chris signature 58 | 59 | 60 | appen, mutate or add? 61 | 62 | JWT.content { 63 | holder : "Chris", 64 | gives : [ 65 | { 66 | holder : "Amy", 67 | giver : { // minter. the root is self signed 68 | signing-key : giver-signing-pubkey, 69 | encryption-key : giver-encryption-pubkey 70 | } 71 | } 72 | ] 73 | }.Chris signature 74 | 75 | 76 | 77 | { 78 | holder : "Chris", 79 | minter-sig : "Chris Sig", 80 | gives : [ 81 | { 82 | holder : "Amy", giver-sig : "Chris Sig", 83 | gives : [{holder : "Irma", 84 | giver-sig : "Amy Sig", 85 | gives : []}] 86 | } 87 | ] 88 | } 89 | 90 | 91 | { 92 | signed : "give:ChrisKey;bh:documenthash|Chris Sig", 93 | gives : [ 94 | { 95 | signed : "give:AmyKey;bh:parenthash|Chris Sig", 96 | gives : [ 97 | {signed : "IsaacKey;bh:parenthash|Amy Sig", 98 | gives : [ 99 | {signed : "JohnKey;bh:parenthash|Isaac Sig"} 100 | ]}, 101 | {signed : "IrmaKey;bh:parenthash|Amy Sig", 102 | gives : []} 103 | ] 104 | } 105 | ] 106 | } 107 | 108 | 109 | ["give:ChrisKey;bh:documenthash|Chris Sig",[]] 110 | 111 | ["give:ChrisKey;bh:documenthash|Chris Sig",[ 112 | ["give:AmyKey;bh:parenthash;prev-id-givetree-blockhash|Chris Sig",[ 113 | ["give:IsaacKey;bh:parenthash|Amy Sig",[]], 114 | ["give:IrmaKey;bh:parenthash|Amy Sig",[]] 115 | ]] 116 | ]] 117 | 118 | 119 | 120 | ethereum program along the coin history to control who can give multiple copies? update validate. 121 | 122 | 123 | history-signatures 124 | - [minter sig, holder1 sig] 125 | 126 | 127 | Irma decrypts the paylod to display it. 128 | 129 | Irma gives the coin to Isaac by encrypting payload for Isaac and, adding him to the holder list (tree?). 130 | 131 | history 132 | [holder3, holder2 signature] 133 | [holder2, holder1 signature] 134 | [holder1, minter signature] 135 | 136 | history-signatures 137 | - [minter sig, holder1 sig] 138 | -------------------------------------------------------------------------------- /test/story.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | const jose = require("node-jose"); 3 | 4 | require("babel-register"); 5 | // var jwt = require('webcrypto-jwt'); 6 | // var jose = require("node-jose"); 7 | var dc = require(".."); 8 | 9 | test("create a wallet", function (t) { 10 | t.plan(2) 11 | var wallet = new dc.Wallet("test"); 12 | t.equals(wallet.name, "test") 13 | 14 | // ensure the wallet has keys 15 | wallet.setupKeys().then(function(){ 16 | t.assert(wallet.signingKey) 17 | }) 18 | }) 19 | 20 | test("mint a coin", function (t) { 21 | t.plan(6) 22 | var wallet = new dc.Wallet("test"); 23 | wallet.setupKeys().then(function(){ 24 | var content = new Buffer("The word is the coin.", "utf8") 25 | wallet.mint(content).then(function (coin) { 26 | t.equals(coin.content, content) 27 | t.equals(coin.coinID.length, "_u2e46oaAUyKVTVGHVaPC_Y4EKL3la7CTvXPLoU3QrY".length) 28 | t.assert(coin.givetree[0], "jose signature") 29 | 30 | jose.JWS.createVerify(coin.mintKey). 31 | verify(coin.givetree[0]). 32 | then(function(decoded) { 33 | t.equals(JSON.parse(decoded.payload)[0], "izwuMySz5A-xSRguhHo42LPqN_39f4osW6ITxUsVxa4") 34 | t.equals(decoded.key, coin.mintKey) 35 | coin.validate().then((valid)=>{ 36 | t.equals(true, valid) 37 | }) 38 | }) 39 | }) 40 | }) 41 | }) 42 | 43 | test("give a coin", function (t) { 44 | t.plan(6) 45 | var wallet = new dc.Wallet("Alice"); 46 | wallet.setupKeys().then(function(){ 47 | var wallet2 = new dc.Wallet("Bob"); 48 | wallet2.setupKeys().then(function() { 49 | var content = new Buffer("The coin is the word.", "utf8") 50 | wallet.mint(content).then(function (coin) { 51 | // console.log("mant", coin) 52 | t.assert(coin.givetree[0], "jose signature") 53 | t.assert(coin.givetree[1], "children") 54 | coin.give(wallet, wallet2.signingKey.toJSON()).then(()=>{ 55 | // console.log("gave", coin.givetree) 56 | t.assert(coin.givetree[1][0], "first child") 57 | t.equals(coin.givetree[1][0][0].length, 480, "first child jose signature") 58 | t.equals(coin.givetree[1][0][1].length, 0, "first child children") 59 | coin.validate().then((valid)=>{ 60 | t.equals(true, valid) 61 | }) 62 | }).catch((e)=>{ 63 | console.log("catch", e) 64 | }) 65 | }) 66 | }) 67 | }) 68 | }) 69 | 70 | 71 | test("give a coin multiple times", function (t) { 72 | t.plan(6) 73 | var wallet = new dc.Wallet("Alice"); 74 | wallet.setupKeys().then(function(){ 75 | var wallet2 = new dc.Wallet("Bob"); 76 | wallet2.setupKeys().then(function() { 77 | var content = new Buffer("The coin is the word.", "utf8") 78 | wallet.mint(content).then(function (coin) { 79 | coin.give(wallet, wallet2.signingKey.toJSON()).then(()=>{ 80 | t.equals(coin.givetree[1][0][1].length, 0, "first child children empty") 81 | var wallet3 = new dc.Wallet("Ace"); 82 | wallet3.setupKeys().then(function() { 83 | coin.give(wallet2, wallet3.signingKey.toJSON()).then(()=>{ 84 | t.equals(coin.givetree[1][0][1].length, 1, "first child children one") 85 | coin.give(wallet2, wallet.signingKey.toJSON()).then(()=>{ 86 | t.equals(coin.givetree[1][0][1].length, 2, "first child children after double give") 87 | t.equals(coin.givetree[1][0][1][0][1].length, 0, "second child children before give") 88 | coin.give(wallet3, wallet.signingKey.toJSON()).then(()=>{ 89 | t.equals(coin.givetree[1][0][1][0][1].length, 1, "second child children after give") 90 | coin.validate().then((valid)=>{ 91 | t.equals(true, valid) 92 | }) 93 | }) 94 | }) 95 | }) 96 | }) 97 | }).catch((e)=>{ 98 | console.log("catch", e) 99 | }) 100 | }) 101 | }) 102 | }) 103 | }) 104 | 105 | test("invalid coin", function (t) { 106 | t.plan(6) 107 | 108 | var wallet = new dc.Wallet("Alice"); 109 | wallet.setupKeys().then(function(){ 110 | var wallet2 = new dc.Wallet("Bob"); 111 | wallet2.setupKeys().then(function() { 112 | var content = new Buffer("The coin is the word.", "utf8") 113 | wallet.mint(content).then(function (coin) { 114 | coin.give(wallet, wallet2.signingKey.toJSON()).then(()=>{ 115 | t.equals(coin.givetree[1][0][1].length, 0, "first child children empty") 116 | var wallet3 = new dc.Wallet("Ace"); 117 | wallet3.setupKeys().then(function() { 118 | coin.give(wallet2, wallet3.signingKey.toJSON()).then(()=>{ 119 | t.equals(coin.givetree[1][0][1].length, 1, "first child children one") 120 | coin.give(wallet2, wallet.signingKey.toJSON()).then(()=>{ 121 | t.equals(coin.givetree[1][0][1].length, 2, "first child children after double give") 122 | t.equals(coin.givetree[1][0][1][0][1].length, 0, "second child children before give") 123 | coin.give(wallet3, wallet.signingKey.toJSON()).then(()=>{ 124 | t.equals(coin.givetree[1][0][1][0][1].length, 1, "second child children after give") 125 | // here we will mess with the give tree by putting one of the signatures in an earlier position 126 | var stolenBlock = coin.givetree[1][0][1].pop(); 127 | coin.givetree[1].push(stolenBlock); 128 | 129 | coin.validate().then((valid)=>{ 130 | t.equals(false, valid) 131 | }) 132 | 133 | }) 134 | }) 135 | }) 136 | }) 137 | }) 138 | }) 139 | }) 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /lib/coin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const jose = require("node-jose"); 3 | const URLSafeBase64 = require('urlsafe-base64'); 4 | 5 | class Coin { 6 | // create a coin with: 7 | // content 8 | // content hash 9 | // create root block 10 | // sign content hash 11 | 12 | constructor() { 13 | // to load a coin that I didn't mint, maybe we load 14 | // from the database and pass in the document here? 15 | } 16 | validate() { 17 | // decrypt content and verify root(s) 18 | 19 | } 20 | mint(content, mintWallet) { 21 | this.content = content; 22 | // todo encrypt content 23 | this.mintKey = mintWallet.signingKey; 24 | 25 | return jose.JWA.digest("SHA-256", this.content).then((contentDigest)=>{ 26 | return this.mintRootBlock(contentDigest, mintWallet).then(()=>{ 27 | return this.rootDigest().then((signatureDigest)=>{ 28 | // The id validates that the coin can only be minted by 29 | // someone who controls the minter keys. 30 | // If the minter mints the same content multiple times the 31 | // history can be merged at the level of the first give. 32 | this.coinID = URLSafeBase64.encode(signatureDigest); 33 | return this; 34 | }) 35 | }) 36 | }) 37 | } 38 | rootDigest() { 39 | const rootBlock = this.givetree[0]; 40 | // console.log("rootBlock",rootBlock,this.givetree); 41 | const blockBuffer = new Buffer(rootBlock, "utf8"); 42 | return jose.JWA.digest("SHA-256", blockBuffer) 43 | } 44 | validateRoot() { 45 | return this.rootDigest().then((signatureDigest) => { 46 | const basedDigest = URLSafeBase64.encode(signatureDigest); 47 | if (this.coinID !== basedDigest) { 48 | throw new Error("invalid coinID " + this.coinID + ", " + basedDigest) 49 | } 50 | }) 51 | } 52 | mintRootBlock(contentDigest, mintWallet) { 53 | const blockData = [URLSafeBase64.encode(contentDigest), mintWallet.signingKey.toJSON()]; 54 | return jose.JWS.createSign({ format: 'compact' }, mintWallet.signingKey). 55 | update(JSON.stringify(blockData), "utf8"). 56 | final(). 57 | then((signedBlock) => { 58 | // The coinID is derived from the root block, and validated, 59 | // so any change in the root block would be a differnt coin. 60 | // There can only be one root 61 | // console.log("signedBlock", signedBlock) 62 | this.givetree = [signedBlock,[]]; 63 | }); 64 | } 65 | mintGiveBlock(parentBlockDigest, receiverKeyJSON, giveWallet) { 66 | const blockData = [URLSafeBase64.encode(parentBlockDigest), receiverKeyJSON]; 67 | return jose.JWS.createSign({ format: 'compact' }, giveWallet.signingKey). 68 | update(JSON.stringify(blockData), "utf8"). 69 | final() 70 | } 71 | give(holder, receiverKeyJSON) { 72 | return this.findHeldBlockNode(holder).then((heldBlockNode) => { 73 | // console.log("heldBlockNode", heldBlockNode) 74 | const rawBlock = heldBlockNode[0]; 75 | const children = heldBlockNode[1]; 76 | console.log(" found give", rawBlock, children) 77 | // TODO validate children.length and coin policy 78 | const blockBuffer = new Buffer(rawBlock, "utf8"); 79 | return jose.JWA.digest("SHA-256", blockBuffer).then((rawBlockDigest)=>{ 80 | return this.mintGiveBlock(rawBlockDigest, receiverKeyJSON, holder).then((block) => { 81 | children.push([block,[]]) 82 | }) 83 | }) 84 | // heldBlockNode[0] 85 | // heldBlockNode[1] append block signed by holder wallet, gives to reciver wallet 86 | }).catch((e)=>{ 87 | console.log("held block not found", e) 88 | }) 89 | } 90 | decodeBlock(block, key) { 91 | return jose.JWS.createVerify(key). 92 | verify(block). 93 | then(function(decoded) { 94 | // console.log(" decodeBlock", decoded) 95 | return decoded; 96 | }) 97 | } 98 | findHeldBlockNode (holder) { 99 | // fold over blocks to find those signed to holder's key\ 100 | console.log("findHeldBlock need to implement fold", holder.signingKey) 101 | return Promise.resolve() 102 | .then( () => { 103 | var myblocks = []; 104 | return this.foldBlockNodes((decodedBlock, rawBlock, children) => { 105 | console.log("decodedBlock", decodedBlock[1].kid, holder.signingKey.kid) 106 | if (decodedBlock[1].kid == holder.signingKey.kid) { 107 | console.log("held Block") 108 | myblocks.push(Promise.resolve([rawBlock, children])) 109 | } 110 | }).then(()=>{ 111 | console.log("myblocks", myblocks) 112 | if (myblocks[0]) { 113 | return myblocks[0]; 114 | } else { 115 | throw new Error("no held block") 116 | } 117 | }) 118 | }) 119 | } 120 | foldBlockNodes(callback) { 121 | return this.foldRootNode().then((results) => { 122 | // const {rootPayload, block, children} = results; 123 | return this.foldBlockNode(callback, results.rootPayload, results.block, results.children) 124 | 125 | }) 126 | } 127 | foldBlockNode(callback, parentPayload, rawParentBlock, children) { 128 | // console.log("foldBlockNode", parentPayload, children, rawParentBlock) 129 | callback(parentPayload, rawParentBlock, children) 130 | // now for each child, decode the child and call foldBlockNode on it 131 | if (children.length > 0) { 132 | return jose.JWK.asKey(parentPayload[1]).then((holderKey) => { 133 | var decoders = children.map((childNode)=>{ 134 | const rawChildBlock = childNode[0]; 135 | const childChildren = childNode[1]; 136 | return this.decodeBlock(rawChildBlock, holderKey).then((decoded) => { 137 | var childPayload = JSON.parse(decoded.payload.toString()) 138 | return this.foldBlockNode(callback, childPayload, rawChildBlock, childChildren) 139 | }) 140 | }) 141 | // console.log("decoders", decoders) 142 | return Promise.all(decoders); 143 | }); 144 | } else { 145 | return true; 146 | } 147 | } 148 | foldRootNode() { 149 | var holderKey = this.mintKey; 150 | var node = this.givetree; 151 | var block = node[0]; 152 | var children = node[1]; 153 | return this.decodeBlock(block, holderKey).then((decoded) => { 154 | var rootPayload = JSON.parse(decoded.payload.toString()) 155 | console.log("folded BLOCK", rootPayload) 156 | return {rootPayload:rootPayload, block:block, children:children}; 157 | }) 158 | // this needs to decode blocks into the decoded tree so we can use the signing 159 | // key to verify child blocks 160 | } 161 | // TODO validate givetree 162 | validate() { 163 | return this.validateRoot().then(()=>{ 164 | return this.foldBlockNodes((decodedBlock, rawBlock, children) => { 165 | console.log("folded BlockNodes", decodedBlock[1].kid, children.length, decodedBlock[0]) 166 | }) 167 | }).then(()=>{ 168 | return true; 169 | }).catch((e)=>{ 170 | console.log("error", e); 171 | return false; 172 | }) 173 | } 174 | } 175 | 176 | module.exports = Coin; 177 | 178 | 179 | // validate/decode the entire tree? decode until we find our wallet 180 | 181 | // root block 182 | 183 | // find blocks wallet X can sign 184 | --------------------------------------------------------------------------------