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

New SEA features! (many new features) #1400

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions examples/webauthn.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<script src="/gun/gun.js"></script>
<script src="/gun/sea.js"></script>
<script>
var gun = new Gun();
var user = gun.user();
</script>
</head>
<body>
<h1>WebAuthn Example</h1>
<button id="create">Create keypass to get public key</button>
<button id="sign">Sign data using passkey</button>
<button id="verify">Verify signature of passkey</button>
<button id="put">Put to self's graph with WebAuthn</button>
<button id="put-with-pair">Put to self's graph with pair</button>
<script src="./webauthn.js"></script>
</body>
</html>
176 changes: 176 additions & 0 deletions examples/webauthn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
DISCUSSION WITH AI:
UPGRADE SEA.verify to allow put with signed

The goal of this dev session is to make the check() function in SEA handle signed puts, but without having the user to be authenticated. It should check if the signature matches the pub, thats it.

There are files that are related to this mission: sign.js, verify.js and index.js, they are in /sea folder.

The sign() function in sign.js create signature from given SEA pair. We will modify it to also be able to request WebAuthn signature. We must transform (normalize) the signature of passkey to make it look like SEA signature. But we must keep its current functionalities remain working.

MUST KEEP IN MIND: webauthn sign doesn't sign the original data alone, instead, it wrap the original data in an object

The verify() function in verify.js verifies if signature matches pub. We will modify it to also be able to verify new kind of signature created by webauthn passkey.

The check() function in index.js handles every data packet that flows through the system. It works like a filter to filter out bad (signature not matched) datas.

We must also modify index.js in sea, the check.pub() function. It handles outgoing and incoming put data. In there we will make it to be able to use SEA.sign with external authenticator which is WebAuthn.

We must edit slowly. After every edition, we must debug on browser using examples/webauthn.html and examples/webauthn.js to check if it works, then keep editing slowly until it works.

What should we edit?
The sea.js in the root folder is just a built, it is very heavy and you cannot read it. So we must "blindly" debug in sign.js, verify.js and index.js in /sea folder.

DO THIS AFTER EVERY EDITION:
npm run buildSea

We need to re-build sea before testing it.

BIG UPDATE:
Now after some coding, the sign.js and verify.js work perfectly in test in webauthn.js. Ok. We should now focus in modifying check.pub in index.js.

How it should work?
At line 147 in index.js, it currently checks:
- if user authenticated (in SEA) and must not have wrapped cert
- if user is writing to his graph
- if he is writing to someone else's graph, must have msg._.msg.opt.cert

Now what we want is to make it to also allows unauthenticated user to make put, using put(data, null, {opt: {authenticator}}).
It should detect if authenticator exists, then use that in replace for user._.sea. Then the following logic is the same. But we also must keep the current functionalities remain working.

What I want?
When putting with authenticator (webauthn), the device doesn't provide public key. So user must provide pub via opt.pub if he wants to put data to someone else's graph. If opt.pub doesn't exist, he can only writes to his own graph.
*/

console.log("WEB AUTHN EXAMPLE")

const base64url = {
encode: function(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
},
decode: function(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return atob(str);
}
};

const data = "Hello, World!"
let credential, pub, signature

document.getElementById('create').onclick = async () => {
try {
credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array(16),
rp: { id: "localhost", name: "Example Inc." },
user: {
id: new TextEncoder().encode("example-user-id"),
name: "Example User",
displayName: "Example User"
},
// See the list of algos: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// The 2 algos below are required in order to work with SEA
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ECDSA, P-256 curve, for signing
{ type: "public-key", alg: -25 }, // ECDH, P-256 curve, for creating shared secrets using SEA.secret
{ type: "public-key", alg: -257 }
],
authenticatorSelection: {
userVerification: "preferred"
},
timeout: 60000,
attestation: "none"
}
});

console.log("Credential:", credential);

const publicKey = credential.response.getPublicKey();
const rawKey = new Uint8Array(publicKey);

console.log("Raw public key bytes:", rawKey);

const xCoord = rawKey.slice(27, 59);
const yCoord = rawKey.slice(59, 91);

console.log("X coordinate (32 bytes):", base64url.encode(xCoord));
console.log("Y coordinate (32 bytes):", base64url.encode(yCoord));

pub = `${base64url.encode(xCoord)}.${base64url.encode(yCoord)}`;
console.log("Final pub format:", pub);

} catch(err) {
console.error('Create credential error:', err);
}
}

const authenticator = async (data) => {
const challenge = new TextEncoder().encode(data);
const options = {
publicKey: {
challenge,
rpId: window.location.hostname,
userVerification: "preferred",
allowCredentials: [{
type: "public-key",
id: credential.rawId
}],
timeout: 60000
}
};

const assertion = await navigator.credentials.get(options);
console.log("SIGNED:", {options, assertion});
return assertion.response;
};

document.getElementById('sign').onclick = async () => {
if (!credential) {
console.error("Create credential first");
return;
}

try {
signature = await SEA.sign(data, authenticator);
console.log("Signature:", signature);
} catch(err) {
console.error('Signing error:', err);
}
}

document.getElementById('verify').onclick = async () => {
if (!signature) {
console.error("Sign message first");
return;
}

try {
const verified = await SEA.verify(signature, pub);
console.log("Verified:", verified);
} catch(err) {
console.error('Verification error:', err);
}
}

document.getElementById('put').onclick = async () => {
gun.get(`~${pub}`).get('test').put("hello world", null, { opt: { authenticator }})
setTimeout(() => {
gun.get(`~${pub}`).get('test').once((data) => {
console.log("Data:", data);
})
}, 2000)
}

document.getElementById('put-with-pair').onclick = async () => {
const bob = await SEA.pair()
gun.get(`~${bob.pub}`).get('test').put("this is bob", null, { opt: { authenticator: bob }})
setTimeout(() => {
gun.get(`~${bob.pub}`).get('test').once((data) => {
console.log("Data:", data);
})
}, 2000)
}
5 changes: 1 addition & 4 deletions gun.js
Original file line number Diff line number Diff line change
Expand Up @@ -758,15 +758,12 @@
Gun.log = function(){ return (!Gun.log.off && C.log.apply(C, arguments)), [].slice.call(arguments).join(' ') };
Gun.log.once = function(w,s,o){ return (o = Gun.log.once)[w] = o[w] || 0, o[w]++ || Gun.log(s) };

if(typeof window !== "undefined"){ (window.GUN = window.Gun = Gun).window = window }
((typeof globalThis !== "undefined" && typeof window === "undefined" && typeof WorkerGlobalScope !== "undefined") ? ((globalThis.GUN = globalThis.Gun = Gun).window = globalThis) : (typeof window !== "undefined" ? ((window.GUN = window.Gun = Gun).window = window) : undefined));
try{ if(typeof MODULE !== "undefined"){ MODULE.exports = Gun } }catch(e){}
module.exports = Gun;

(Gun.window||{}).console = (Gun.window||{}).console || {log: function(){}};
(C = console).only = function(i, s){ return (C.only.i && i === C.only.i && C.only.i++) && (C.log.apply(C, arguments) || s) };

;"Please do not remove welcome log unless you are paying for a monthly sponsorship, thanks!";
Gun.log.once("welcome", "Hello wonderful person! :) Thanks for using GUN, please ask for help on http://chat.gun.eco if anything takes you longer than 5min to figure out!");
})(USE, './root');

;USE(function(module){
Expand Down
2 changes: 1 addition & 1 deletion gun.min.js

Large diffs are not rendered by default.

138 changes: 138 additions & 0 deletions lib/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
var fs = require('fs');
var nodePath = require('path');

var dir = __dirname + '/../';

function read(path) {
return fs.readFileSync(nodePath.join(dir, path)).toString();
}

function write(path, data) {
return fs.writeFileSync(nodePath.join(dir, path), data);
}

// The order of modules matters due to dependencies
const seaModules = [
'root',
'https',
'base64',
'array',
'buffer',
'shim',
'settings',
'sha256',
'sha1',
'work',
'pair',
'sign',
'verify',
'aeskey',
'encrypt',
'decrypt',
'secret',
'certify',
'sea',
'user',
'then',
'create',
'auth',
'recall',
'share',
'index'
];

function normalizeContent(code) {
// Remove IIFE wrapper if present
code = code.replace(/^\s*;?\s*\(\s*function\s*\(\s*\)\s*\{/, '');
code = code.replace(/\}\s*\(\s*\)\s*\)?\s*;?\s*$/, '');

// Split into lines and remove common indentation
const lines = code.split('\n');
let minIndent = Infinity;

// Find minimum indentation (ignoring empty lines)
lines.forEach(line => {
if (line.trim().length > 0) {
const indent = line.match(/^\s*/)[0].length;
minIndent = Math.min(minIndent, indent);
}
});

// Remove common indentation
const cleanedLines = lines.map(line => {
if (line.trim().length > 0) {
return line.slice(minIndent);
}
return '';
});

return cleanedLines.join('\n').trim();
}

function buildSea(arg) {
if (arg !== 'sea') {
console.error('Only "sea" argument is supported');
process.exit(1);
}

// Start with the USE function definition
let output = `;(function(){

/* UNBUILD */
function USE(arg, req){
return req? require(arg) : arg.slice? USE[R(arg)] : function(mod, path){
arg(mod = {exports: {}});
USE[R(path)] = mod.exports;
}
function R(p){
return p.split('/').slice(-1).toString().replace('.js','');
}
}
if(typeof module !== "undefined"){ var MODULE = module }
/* UNBUILD */\n\n`;

// Add each module wrapped in USE()
seaModules.forEach(name => {
try {
let code = read('sea/' + name + '.js');

// Clean up the code
code = normalizeContent(code);

// Replace require() with USE(), but skip any requires within UNBUILD comments
let inUnbuild = false;
const lines = code.split('\n').map(line => {
if (line.includes('/* UNBUILD */')) {
inUnbuild = !inUnbuild;
return line;
}
if (!inUnbuild) {
return line.replace(/require\(/g, 'USE(');
}
return line;
});
code = lines.join('\n');

// Add module with consistent indentation
output += ` ;USE(function(module){\n`;
output += code.split('\n').map(line => line.length ? ' ' + line : '').join('\n');
output += `\n })(USE, './${name}');\n\n`;
} catch(e) {
console.error('Error processing ' + name + '.js:', e);
}
});

// Close IIFE
output += '}());';

// Write output
write('sea.js', output);
console.log('Built sea.js');
}

if (require.main === module) {
const arg = process.argv[2];
buildSea(arg);
}

module.exports = buildSea;
1 change: 1 addition & 0 deletions lib/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function serve(req, res, next){ var tmp;
}
var S = +new Date;
var rs = fs.createReadStream(path);
if(req.url.slice(-3) === '.js'){ res.writeHead(200, {'Content-Type': 'text/javascript'}) }
rs.on('open', function(){ console.STAT && console.STAT(S, +new Date - S, 'serve file open'); rs.pipe(res) });
rs.on('error', function(err){ res.end(404+'') });
rs.on('end', function(){ console.STAT && console.STAT(S, +new Date - S, 'serve file end') });
Expand Down
Loading