New SEA features! (many new features) (#1400)

* feat: create pair with seed, content addressing with shorter hash

* feat: create pair using priv/epriv

* optimize SEA.pair

* feat: globalThis along with window

* white labeling

* feat: add WebAuthn example and enhance SEA.sign, SEA.verify, SEA check.pub, for WebAuthn support

* feat: enhance WebAuthn integration with new put options and improved signature handling

* polish SEA.sign and SEA.verify

* feat: localize options in SEA.check.pub to enhance security and prevent attacks

* fix: correct destructuring of user object to enhance security in SEA

* rebuild SEA

* feat: support ArrayBuffer as seed for key pair generation in SEA

* test: add unit test for hashing ArrayBuffer in SEA

* fix: create deterministic key pair from seed

* fix: add missing B parameter for ECC curve and implement point validation

* feat: add ArrayBuffer support for hashing in SEA and implement corresponding unit test

* fix: convert numeric salt to string in PBKDF2 implementation

* fix: convert numeric salt option to string in PBKDF2 implementation

* improve hashing tests

* improve sea.work

* rebuild SEA

* improve SEA.work and rebuild SEA

* enhance SEA encryption handling and improve test coverage for SEA functions

---------

Co-authored-by: noname <x@null.com>
Co-authored-by: x <x@mimiza.com>
Co-authored-by: x <null>
Co-authored-by: noname <no@name.com>
This commit is contained in:
akaoio 2025-03-25 01:41:36 +07:00 committed by GitHub
parent ff4bf9293c
commit f0cce073a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2271 additions and 802 deletions

20
examples/webauthn.html Normal file
View File

@ -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
examples/webauthn.js Normal file
View File

@ -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
gun.js
View File

@ -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){

2
gun.min.js vendored

File diff suppressed because one or more lines are too long

138
lib/build.js Normal file
View File

@ -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;

View File

@ -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') });

1189
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,11 +13,12 @@
"https": "HTTPS_KEY=test/https/server.key HTTPS_CERT=test/https/server.crt npm start",
"prepublishOnly": "npm run unbuild",
"test": "echo 'Did you run PANIC holy-grail, 1~X, on-recover, etc.?' && mocha",
"testsea": "mocha test/sea/sea.js",
"testSea": "mocha test/sea/sea.js",
"e2e": "mocha e2e/distributed.js",
"docker": "hooks/build",
"minify": "uglifyjs gun.js -o gun.min.js -c -m",
"unbuild": "node lib/unbuild.js & npm run minify",
"buildSea": "node lib/build.js sea",
"unbuildSea": "node lib/unbuild.js sea",
"unbuildMeta": "node lib/unbuild.js lib/meta"
},

559
sea.js
View File

@ -19,8 +19,7 @@
// IT IS IMPLEMENTED IN A POLYFILL/SHIM APPROACH.
// THIS IS AN EARLY ALPHA!
if(typeof self !== "undefined"){ module.window = self } // should be safe for at least browser/worker/nodejs, need to check other envs like RN etc.
if(typeof window !== "undefined"){ module.window = window }
module.window = (typeof globalThis !== "undefined" && typeof window === "undefined" && typeof WorkerGlobalScope !== "undefined") ? globalThis : (typeof window !== "undefined" ? window : undefined);
var tmp = module.window || module, u;
var SEA = tmp.SEA || {};
@ -231,7 +230,7 @@
if(d){ jwk.d = d }
return jwk;
};
s.keyToJwk = function(keyBytes) {
const keyB64 = keyBytes.toString('base64');
const k = keyB64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
@ -288,12 +287,19 @@
cb = salt;
salt = u;
}
data = (typeof data == 'string')? data : await shim.stringify(data);
// Check if data is an ArrayBuffer, if so use Uint8Array to access the data
if(data instanceof ArrayBuffer){
data = new Uint8Array(data);
data = new shim.TextDecoder("utf-8").decode(data);
}
data = (typeof data == 'string') ? data : await shim.stringify(data);
if('sha' === (opt.name||'').toLowerCase().slice(0,3)){
var rsha = shim.Buffer.from(await sha(data, opt.name), 'binary').toString(opt.encode || 'base64')
if(cb){ try{ cb(rsha) }catch(e){console.log(e)} }
return rsha;
}
if (typeof salt === "number") salt = salt.toString();
if (typeof opt.salt === "number") opt.salt = opt.salt.toString();
salt = salt || shim.random(9);
var key = await (shim.ossl || shim.subtle).importKey('raw', new shim.TextEncoder().encode(data), {name: opt.name || 'PBKDF2'}, false, ['deriveBits']);
var work = await (shim.ossl || shim.subtle).deriveBits({
@ -320,71 +326,168 @@
;USE(function(module){
var SEA = USE('./root');
var shim = USE('./shim');
var S = USE('./settings');
SEA.name = SEA.name || (async (cb, opt) => { try {
if(cb){ try{ cb() }catch(e){console.log(e)} }
return;
} catch(e) {
console.log(e);
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
return;
}});
// P-256 curve constants
const n = BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551");
const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b"); // Missing B parameter
const G = {
x: BigInt("0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"),
y: BigInt("0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5")
};
// Core ECC functions
function mod(a, m) { return ((a % m) + m) % m; }
// Constant-time modular inverse using Fermat's Little Theorem (p is prime)
function modInv(a, p) {
// a^(p-2) mod p
return modPow(a, p - BigInt(2), p);
}
// Constant-time modular exponentiation (square-and-multiply)
function modPow(base, exponent, modulus) {
if (modulus === BigInt(1)) return BigInt(0);
base = mod(base, modulus);
let result = BigInt(1);
while (exponent > BigInt(0)) {
if (exponent & BigInt(1)) {
result = mod(result * base, modulus);
}
exponent >>= BigInt(1);
base = mod(base * base, modulus);
}
return result;
}
// Verify a point is on the curve
function isOnCurve(point) {
if (!point) return false;
// y² = x³ + ax + b (mod p)
const { x, y } = point;
const left = mod(y * y, P);
const right = mod(mod(mod(x * x, P) * x, P) + mod(A * x, P) + B, P);
return left === right;
}
function pointAdd(p1, p2) {
if (p1 === null) return p2; if (p2 === null) return p1;
if (p1.x === p2.x && mod(p1.y + p2.y, P) === 0n) return null;
let lambda = p1.x === p2.x && p1.y === p2.y
? mod((3n * mod(p1.x ** 2n, P) + A) * modInv(2n * p1.y, P), P)
: mod((mod(p2.y - p1.y, P)) * modInv(mod(p2.x - p1.x, P), P), P);
const x3 = mod(lambda ** 2n - p1.x - p2.x, P);
return { x: x3, y: mod(lambda * mod(p1.x - x3, P) - p1.y, P) };
}
function pointMult(k, point) {
let r = null, a = point;
while (k > 0n) {
if (k & 1n) r = pointAdd(r, a);
a = pointAdd(a, a);
k >>= 1n;
}
return r;
}
//SEA.pair = async (data, proof, cb) => { try {
SEA.pair = SEA.pair || (async (cb, opt) => { try {
opt = opt || {};
const subtle = shim.subtle, ecdhSubtle = shim.ossl || subtle;
let r = {};
var ecdhSubtle = shim.ossl || shim.subtle;
// First: ECDSA keys for signing/verifying...
var sa = await shim.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, [ 'sign', 'verify' ])
.then(async (keys) => {
// privateKey scope doesn't leak out from here!
//const { d: priv } = await shim.subtle.exportKey('jwk', keys.privateKey)
var key = {};
key.priv = (await shim.subtle.exportKey('jwk', keys.privateKey)).d;
var pub = await shim.subtle.exportKey('jwk', keys.publicKey);
//const pub = Buff.from([ x, y ].join(':')).toString('base64') // old
key.pub = pub.x+'.'+pub.y; // new
// x and y are already base64
// pub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt)
// but split on a non-base64 letter.
return key;
})
// To include PGPv4 kind of keyId:
// const pubId = await SEA.keyid(keys.pub)
// Next: ECDH keys for encryption/decryption...
// Helper functions
const b64ToBI = s => {
let b64 = s.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
return BigInt('0x' + shim.Buffer.from(b64, 'base64').toString('hex'));
};
const biToB64 = n => shim.Buffer.from(n.toString(16).padStart(64, '0'), 'hex')
.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const pubFromPriv = priv => {
const pub = pointMult(priv, G);
if (!isOnCurve(pub)) throw new Error("Invalid point generated");
return biToB64(pub.x) + '.' + biToB64(pub.y);
};
const seedToKey = async (seed, salt) => {
const enc = new shim.TextEncoder();
const buf = typeof seed === 'string' ? enc.encode(seed).buffer :
seed instanceof ArrayBuffer ? seed :
seed && seed.byteLength !== undefined ? (seed.buffer || seed) : null;
if (!buf) throw new Error("Invalid seed");
const combined = new Uint8Array(buf.byteLength + enc.encode(salt).buffer.byteLength);
combined.set(new Uint8Array(buf), 0);
combined.set(new Uint8Array(enc.encode(salt).buffer), buf.byteLength);
const hash = await subtle.digest("SHA-256", combined.buffer);
let priv = BigInt("0x" + Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, "0")).join("")) % n;
if (priv <= 0n || priv >= n) priv = (priv + 1n) % n;
return priv;
};
try{
var dh = await ecdhSubtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveKey'])
.then(async (keys) => {
// privateKey scope doesn't leak out from here!
var key = {};
key.epriv = (await ecdhSubtle.exportKey('jwk', keys.privateKey)).d;
var pub = await ecdhSubtle.exportKey('jwk', keys.publicKey);
//const epub = Buff.from([ ex, ey ].join(':')).toString('base64') // old
key.epub = pub.x+'.'+pub.y; // new
// ex and ey are already base64
// epub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt)
// but split on a non-base64 letter.
return key;
})
}catch(e){
if(SEA.window){ throw e }
if(e == 'Error: ECDH is not a supported algorithm'){ console.log('Ignoring ECDH...') }
else { throw e }
} dh = dh || {};
if (opt.priv) {
const priv = b64ToBI(opt.priv);
r = { priv: opt.priv, pub: pubFromPriv(priv) };
if (opt.epriv) {
r.epriv = opt.epriv;
r.epub = pubFromPriv(b64ToBI(opt.epriv));
} else {
try {
const dh = await ecdhSubtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveKey'])
.then(async k => ({
epriv: (await ecdhSubtle.exportKey('jwk', k.privateKey)).d,
epub: (await ecdhSubtle.exportKey('jwk', k.publicKey)).x + '.' +
(await ecdhSubtle.exportKey('jwk', k.publicKey)).y
}));
r.epriv = dh.epriv; r.epub = dh.epub;
} catch(e) {}
}
} else if (opt.epriv) {
r = { epriv: opt.epriv, epub: pubFromPriv(b64ToBI(opt.epriv)) };
if (opt.priv) {
r.priv = opt.priv;
r.pub = pubFromPriv(b64ToBI(opt.priv));
} else {
const sa = await subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(async k => ({
priv: (await subtle.exportKey('jwk', k.privateKey)).d,
pub: (await subtle.exportKey('jwk', k.publicKey)).x + '.' +
(await subtle.exportKey('jwk', k.publicKey)).y
}));
r.priv = sa.priv; r.pub = sa.pub;
}
} else if (opt.seed) {
const signPriv = await seedToKey(opt.seed, "-sign");
const encPriv = await seedToKey(opt.seed, "-encrypt");
r = {
priv: biToB64(signPriv), pub: pubFromPriv(signPriv),
epriv: biToB64(encPriv), epub: pubFromPriv(encPriv)
};
} else {
const sa = await subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(async k => ({
priv: (await subtle.exportKey('jwk', k.privateKey)).d,
pub: (await subtle.exportKey('jwk', k.publicKey)).x + '.' +
(await subtle.exportKey('jwk', k.publicKey)).y
}));
r = { pub: sa.pub, priv: sa.priv };
try {
const dh = await ecdhSubtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveKey'])
.then(async k => ({
epriv: (await ecdhSubtle.exportKey('jwk', k.privateKey)).d,
epub: (await ecdhSubtle.exportKey('jwk', k.publicKey)).x + '.' +
(await ecdhSubtle.exportKey('jwk', k.publicKey)).y
}));
r.epub = dh.epub; r.epriv = dh.epriv;
} catch(e) {}
}
var r = { pub: sa.pub, priv: sa.priv, /* pubId, */ epub: dh.epub, epriv: dh.epriv }
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
if(cb) try{ cb(r) }catch(e){ console.log(e) }
return r;
} catch(e) {
console.log(e);
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
if(SEA.throw) throw e;
if(cb) cb();
return;
}});
@ -398,35 +501,60 @@
var sha = USE('./sha256');
var u;
async function n(r, o, c) {
try {
if(!o.raw){ r = 'SEA' + await shim.stringify(r) }
if(c){ try{ c(r) }catch(e){} }
return r;
} catch(e) { return r }
}
async function w(r, j, o, c) {
var x = {
m: j,
s: r.signature ? shim.Buffer.from(r.signature, 'binary').toString(o.encode || 'base64') : u,
a: shim.Buffer.from(r.authenticatorData, 'binary').toString('base64'),
c: shim.Buffer.from(r.clientDataJSON, 'binary').toString('base64')
};
if (!x.s || !x.a || !x.c) throw "WebAuthn signature invalid";
return n(x, o, c);
}
async function k(p, j, o, c) {
var x = S.jwk(p.pub, p.priv);
if (!x) throw "Invalid key pair";
var h = await sha(j);
var s = await (shim.ossl || shim.subtle).importKey('jwk', x, S.ecdsa.pair, false, ['sign'])
.then((k) => (shim.ossl || shim.subtle).sign(S.ecdsa.sign, k, new Uint8Array(h)))
.catch(() => { throw "SEA signature failed" });
return n({m: j, s: shim.Buffer.from(s, 'binary').toString(o.encode || 'base64')}, o, c);
}
SEA.sign = SEA.sign || (async (data, pair, cb, opt) => { try {
opt = opt || {};
if(!(pair||opt).priv){
if(!SEA.I){ throw 'No signing key.' }
if(u === data) throw '`undefined` not allowed.';
if(!(pair||opt).priv && typeof pair !== 'function'){
if(!SEA.I) throw 'No signing key.';
pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why});
}
if(u === data){ throw '`undefined` not allowed.' }
var json = await S.parse(data);
var check = opt.check = opt.check || json;
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
&& u !== await SEA.verify(check, pair)){ // don't sign if we already signed it.
var r = await S.parse(check);
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
}
var pub = pair.pub;
var priv = pair.priv;
var jwk = S.jwk(pub, priv);
var hash = await sha(json);
var sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['sign'])
.then((key) => (shim.ossl || shim.subtle).sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, new Uint8Array(hash))) // privateKey scope doesn't leak out from here!
var r = {m: json, s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')}
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
var j = await S.parse(data);
var c = opt.check = opt.check || j;
if(SEA.verify && (S.check(c) || (c && c.s && c.m))
&& u !== await SEA.verify(c, pair)){
return n(await S.parse(c), opt, cb);
}
if(typeof pair === 'function') {
var r = await pair(data);
return r.authenticatorData ? w(r, j, opt, cb) :
n({m: j, s: typeof r === 'string' ? r :
r.signature && shim.Buffer.from(r.signature, 'binary').toString(opt.encode || 'base64')}, opt, cb);
}
return k(pair, j, opt, cb);
} catch(e) {
console.log(e);
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
@ -443,34 +571,97 @@
var sha = USE('./sha256');
var u;
SEA.verify = SEA.verify || (async (data, pair, cb, opt) => { try {
var json = await S.parse(data);
if(false === pair){ // don't verify!
var raw = await S.parse(json.m);
if(cb){ try{ cb(raw) }catch(e){console.log(e)} }
return raw;
async function w(j, k, s) {
var a = new Uint8Array(shim.Buffer.from(j.a, 'base64'));
var c = shim.Buffer.from(j.c, 'base64').toString('utf8');
var m = new TextEncoder().encode(j.m);
var e = btoa(String.fromCharCode(...new Uint8Array(m))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
if (JSON.parse(c).challenge !== e) throw "Challenge verification failed";
var h = await (shim.ossl || shim.subtle).digest(
{name: 'SHA-256'},
new TextEncoder().encode(c)
);
var d = new Uint8Array(a.length + h.byteLength);
d.set(a);
d.set(new Uint8Array(h), a.length);
if (s[0] !== 0x30) throw "Invalid DER signature format";
var o = 2, r = new Uint8Array(64);
for(var i = 0; i < 2; i++) {
var l = s[o + 1];
o += 2;
if (s[o] === 0x00) { o++; l--; }
var p = new Uint8Array(32).fill(0);
p.set(s.slice(o, o + l), 32 - l);
r.set(p, i * 32);
o += l;
}
opt = opt || {};
// SEA.I // verify is free! Requires no user permission.
var pub = pair.pub || pair;
var key = SEA.opt.slow_leak? await SEA.opt.slow_leak(pub) : await (shim.ossl || shim.subtle).importKey('jwk', S.jwk(pub), {name: 'ECDSA', namedCurve: 'P-256'}, false, ['verify']);
var hash = await sha(json.m);
var buf, sig, check, tmp; try{
buf = shim.Buffer.from(json.s, opt.encode || 'base64'); // NEW DEFAULT!
sig = new Uint8Array(buf);
check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash));
if(!check){ throw "Signature did not match." }
}catch(e){
if(SEA.opt.fallback){
return await SEA.opt.fall_verify(data, pair, cb, opt);
}
}
var r = check? await S.parse(json.m) : u;
return (shim.ossl || shim.subtle).verify({ name: 'ECDSA', hash: {name: 'SHA-256'} }, k, r, d);
}
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
async function v(j, k, s, h) {
return (shim.ossl || shim.subtle).verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
k, s, new Uint8Array(h)
);
}
SEA.verify = SEA.verify || (async (d, p, cb, o) => { try {
var j = await S.parse(d);
if(false === p) return cb ? cb(await S.parse(j.m)) : await S.parse(j.m);
o = o || {};
var pub = p.pub || p;
var [x, y] = pub.split('.');
try {
var k = await (shim.ossl || shim.subtle).importKey('jwk', {
kty: 'EC', crv: 'P-256', x, y, ext: true, key_ops: ['verify']
}, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['verify']);
var h = await sha(j.m);
var s = new Uint8Array(shim.Buffer.from(j.s || '', o.encode || 'base64'));
var c = j.a && j.c ? await w(j, k, s) : await v(j, k, s, h);
if(!c) throw "Signature did not match";
// Parse the message content
var r = await S.parse(j.m);
// Handle encrypted data consistently
// SEA encrypted data can be in two formats:
// 1. A string starting with 'SEA' followed by JSON (e.g., 'SEA{"ct":"...","iv":"...","s":"..."}')
// 2. An object with ct, iv, and s properties
// Case 1: Original message was already in SEA string format
if(typeof j.m === 'string' && j.m.startsWith('SEA{')) {
if(cb){ try{ cb(j.m) }catch(e){} }
return j.m;
}
// Case 2: Result is an encrypted data object
// This ensures consistent formatting of encrypted data as SEA strings
if(r && typeof r === 'object' &&
typeof r.ct === 'string' &&
typeof r.iv === 'string' &&
typeof r.s === 'string') {
// Format as standard SEA encrypted string
var seaStr = 'SEA' + JSON.stringify(r);
if(cb){ try{ cb(seaStr) }catch(e){} }
return seaStr;
}
// Default case: Return parsed result as is
if(cb){ try{ cb(r) }catch(e){} }
return r;
} catch(e) {
if(SEA.opt.fallback){
return await SEA.opt.fall_verify(d, p, cb, o);
}
if(cb){ cb() }
return;
}
} catch(e) {
console.log(e); // mismatched owner FOR MARTTI
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
@ -478,43 +669,51 @@
}});
module.exports = SEA.verify;
// legacy & ossl memory leak mitigation:
var knownKeys = {};
var keyForPair = SEA.opt.slow_leak = pair => {
SEA.opt.slow_leak = pair => {
if (knownKeys[pair]) return knownKeys[pair];
var jwk = S.jwk(pair);
knownKeys[pair] = (shim.ossl || shim.subtle).importKey("jwk", jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ["verify"]);
return knownKeys[pair];
};
var O = SEA.opt;
SEA.opt.fall_verify = async function(data, pair, cb, opt, f){
if(f === SEA.opt.fallback){ throw "Signature did not match" } f = f || 1;
if(f === SEA.opt.fallback){ throw "Signature did not match" }
var tmp = data||'';
data = SEA.opt.unpack(data) || data;
var json = await S.parse(data), pub = pair.pub || pair, key = await SEA.opt.slow_leak(pub);
var hash = (f <= SEA.opt.fallback)? shim.Buffer.from(await shim.subtle.digest({name: 'SHA-256'}, new shim.TextEncoder().encode(await S.parse(json.m)))) : await sha(json.m); // this line is old bad buggy code but necessary for old compatibility.
var buf; var sig; var check; try{
buf = shim.Buffer.from(json.s, opt.encode || 'base64') // NEW DEFAULT!
sig = new Uint8Array(buf)
check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash))
if(!check){ throw "Signature did not match." }
}catch(e){ try{
buf = shim.Buffer.from(json.s, 'utf8') // AUTO BACKWARD OLD UTF8 DATA!
sig = new Uint8Array(buf)
check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash))
}catch(e){
if(!check){ throw "Signature did not match." }
}
var json = await S.parse(data), key = await SEA.opt.slow_leak(pair.pub || pair);
var hash = (!f || f <= SEA.opt.fallback)?
shim.Buffer.from(await shim.subtle.digest({name: 'SHA-256'},
new shim.TextEncoder().encode(await S.parse(json.m)))) : await sha(json.m);
try {
var buf = shim.Buffer.from(json.s, opt.encode || 'base64');
var sig = new Uint8Array(buf);
var check = await (shim.ossl || shim.subtle).verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
key, sig, new Uint8Array(hash)
);
if(!check) throw "";
} catch(e) {
try {
buf = shim.Buffer.from(json.s, 'utf8');
sig = new Uint8Array(buf);
check = await (shim.ossl || shim.subtle).verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
key, sig, new Uint8Array(hash)
);
if(!check) throw "";
} catch(e){ throw "Signature did not match." }
}
var r = check? await S.parse(json.m) : u;
O.fall_soul = tmp['#']; O.fall_key = tmp['.']; O.fall_val = data; O.fall_state = tmp['>'];
var r = check ? await S.parse(json.m) : u;
SEA.opt.fall_soul = tmp['#']; SEA.opt.fall_key = tmp['.'];
SEA.opt.fall_val = data; SEA.opt.fall_state = tmp['>'];
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
}
SEA.opt.fallback = 2;
})(USE, './verify');
;USE(function(module){
@ -527,7 +726,7 @@
opt = opt || {};
const combo = key + (salt || shim.random(8)).toString('utf8'); // new
const hash = shim.Buffer.from(await sha256hash(combo), 'binary')
const jwkKey = S.keyToJwk(hash)
return await shim.subtle.importKey('jwk', jwkKey, {name:'AES-GCM'}, false, ['encrypt', 'decrypt'])
}
@ -685,7 +884,6 @@
"cb": A callback function after all things are done.
"opt": If opt.expiry (a timestamp) is set, SEA won't sync data after opt.expiry. If opt.block is set, SEA will look for block before syncing.
*/
console.log('SEA.certify() is an early experimental community supported method that may change API behavior without warning in any future version.')
certificants = (() => {
var data = []
@ -866,7 +1064,7 @@
var pass = pair && (pair.pub || pair.epub) ? pair : alias && typeof args[1] === 'string' ? args[1] : null;
var cb = args.filter(arg => typeof arg === 'function')[0] || null; // cb now can stand anywhere, after alias/pass or pair
var opt = args && args.length > 1 && typeof args[args.length-1] === 'object' ? args[args.length-1] : {}; // opt is always the last parameter which typeof === 'object' and stands after cb
var gun = this, cat = (gun._), root = gun.back(-1);
cb = cb || noop;
opt = opt || {};
@ -973,13 +1171,13 @@
var retries = typeof opt.retries === 'number' ? opt.retries : 9;
var gun = this, cat = (gun._), root = gun.back(-1);
if(cat.ing){
(cb || noop)({err: Gun.log("User is already being created or authenticated!"), wait: true});
return gun;
}
cat.ing = true;
var act = {}, u;
act.a = function(data){
if(!data){ return act.b() }
@ -1340,7 +1538,7 @@
return; // omit!
}
}
if('~@' === soul){ // special case for shared system data, the list of aliases.
check.alias(eve, msg, val, key, soul, at, no); return;
}
@ -1357,18 +1555,23 @@
check.any(eve, msg, val, key, soul, at, no, at.user||''); return;
eve.to.next(msg); // not handled
}
check.hash = function(eve, msg, val, key, soul, at, no){ // mark unbuilt @i001962 's epic hex contrib!
SEA.work(val, null, function(data){
function hexToBase64(hexStr) {
let base64 = "";
for(let i = 0; i < hexStr.length; i++) {
base64 += !(i - 1 & 1) ? String.fromCharCode(parseInt(hexStr.substring(i - 1, i + 1), 16)) : ""}
return btoa(base64);}
if(data && data === key.split('#').slice(-1)[0]){ return eve.to.next(msg) }
else if (data && data === hexToBase64(key.split('#').slice(-1)[0])){
return eve.to.next(msg) }
// Verify content-addressed data matches its hash
check.hash = function (eve, msg, val, key, soul, at, no) {
function base64ToHex(data) {
var binaryStr = atob(data);
var a = [];
for (var i = 0; i < binaryStr.length; i++) {
var hex = binaryStr.charCodeAt(i).toString(16);
a.push(hex.length === 1 ? "0" + hex : hex);
}
return a.join("");
}
var hash = key.split('#').pop();
SEA.work(val, null, function (b64hash) {
var hexhash = base64ToHex(b64hash), b64slice = b64hash.slice(-20), hexslice = hexhash.slice(-20);
if ([b64hash, b64slice, hexhash, hexslice].some(item => item.endsWith(hash))) return eve.to.next(msg);
no("Data hash not same as hash!");
}, {name: 'SHA-256'});
}, { name: 'SHA-256' });
}
check.alias = function(eve, msg, val, key, soul, at, no){ // Example: {_:#~@, ~@alice: {#~@alice}}
if(!val){ return no("Data must exist!") } // data MUST exist
@ -1381,7 +1584,6 @@
no("Alias not same!"); // that way nobody can tamper with the list of public keys.
};
check.pub = async function(eve, msg, val, key, soul, at, no, user, pub){ var tmp // Example: {_:#~asdf, hello:'world'~fdsa}}
const raw = await S.parse(val) || {}
const verify = (certificate, certificant, cb) => {
if (certificate.m && certificate.s && certificant && pub)
// now verify certificate
@ -1415,43 +1617,56 @@
})
return
}
const next = () => {
JSON.stringifyAsync(msg.put[':'], function(err,s){
if(err){ return no(err || "Stringify error.") }
msg.put[':'] = s;
return eve.to.next(msg);
})
}
// Localize some opt props, and delete the original refs to prevent possible attacks
const opt = (msg._.msg || {}).opt || {}
const authenticator = opt.authenticator || (user._ || {}).sea;
const upub = opt.authenticator ? (opt.pub || (user.is || {}).pub || pub) : (user.is || {}).pub;
const cert = opt.cert;
delete opt.authenticator; delete opt.pub;
const raw = await S.parse(val) || {}
if ('pub' === key && '~' + pub === soul) {
if (val === pub) return eve.to.next(msg) // the account MUST match `pub` property that equals the ID of the public key.
return no("Account not same!")
}
if ((tmp = user.is) && tmp.pub && !raw['*'] && !raw['+'] && (pub === tmp.pub || (pub !== tmp.pub && ((msg._.msg || {}).opt || {}).cert))){
if ((user.is || authenticator) && upub && !raw['*'] && !raw['+'] && (pub === upub || (pub !== upub && cert))){
SEA.opt.pack(msg.put, packed => {
SEA.sign(packed, (user._).sea, async function(data) {
// Validate authenticator
if (!authenticator) return no("Missing authenticator");
SEA.sign(packed, authenticator, async function(data) {
if (u === data) return no(SEA.err || 'Signature fail.')
// Validate signature format
if (!data.m || !data.s) return no('Invalid signature format')
msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s}
msg.put['='] = tmp
// if writing to own graph, just allow it
if (pub === user.is.pub) {
if (pub === upub) {
if (tmp = link_is(val)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1
JSON.stringifyAsync(msg.put[':'], function(err,s){
if(err){ return no(err || "Stringify error.") }
msg.put[':'] = s;
return eve.to.next(msg);
})
next()
return
}
// if writing to other's graph, check if cert exists then try to inject cert into put, also inject self pub so that everyone can verify the put
if (pub !== user.is.pub && ((msg._.msg || {}).opt || {}).cert) {
const cert = await S.parse(msg._.msg.opt.cert)
if (pub !== upub && cert) {
const _cert = await S.parse(cert)
// even if cert exists, we must verify it
if (cert && cert.m && cert.s)
verify(cert, user.is.pub, _ => {
msg.put[':']['+'] = cert // '+' is a certificate
msg.put[':']['*'] = user.is.pub // '*' is pub of the user who puts
JSON.stringifyAsync(msg.put[':'], function(err,s){
if(err){ return no(err || "Stringify error.") }
msg.put[':'] = s;
return eve.to.next(msg);
})
if (_cert && _cert.m && _cert.s)
verify(_cert, upub, _ => {
msg.put[':']['+'] = _cert // '+' is a certificate
msg.put[':']['*'] = upub // '*' is pub of the user who puts
next()
return
})
}
@ -1465,7 +1680,7 @@
data = SEA.opt.unpack(data);
if (u === data) return no("Unverified data.") // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account.
if ((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1
// check if cert ('+') and putter's pub ('*') exist
if (raw['+'] && raw['+']['m'] && raw['+']['s'] && raw['*'])
// now verify certificate
@ -1535,6 +1750,6 @@
SEA.opt.shuffle_attack = 1546329600000; // Jan 1, 2019
var fl = Math.floor; // TODO: Still need to fix inconsistent state issue.
// TODO: Potential bug? If pub/priv key starts with `-`? IDK how possible.
})(USE, './index');
}());
}());

View File

@ -9,7 +9,7 @@
opt = opt || {};
const combo = key + (salt || shim.random(8)).toString('utf8'); // new
const hash = shim.Buffer.from(await sha256hash(combo), 'binary')
const jwkKey = S.keyToJwk(hash)
return await shim.subtle.importKey('jwk', jwkKey, {name:'AES-GCM'}, false, ['encrypt', 'decrypt'])
}

View File

@ -11,13 +11,13 @@
var retries = typeof opt.retries === 'number' ? opt.retries : 9;
var gun = this, cat = (gun._), root = gun.back(-1);
if(cat.ing){
(cb || noop)({err: Gun.log("User is already being created or authenticated!"), wait: true});
return gun;
}
cat.ing = true;
var act = {}, u;
act.a = function(data){
if(!data){ return act.b() }

View File

@ -12,7 +12,6 @@
"cb": A callback function after all things are done.
"opt": If opt.expiry (a timestamp) is set, SEA won't sync data after opt.expiry. If opt.block is set, SEA will look for block before syncing.
*/
console.log('SEA.certify() is an early experimental community supported method that may change API behavior without warning in any future version.')
certificants = (() => {
var data = []

View File

@ -9,7 +9,7 @@
var pass = pair && (pair.pub || pair.epub) ? pair : alias && typeof args[1] === 'string' ? args[1] : null;
var cb = args.filter(arg => typeof arg === 'function')[0] || null; // cb now can stand anywhere, after alias/pass or pair
var opt = args && args.length > 1 && typeof args[args.length-1] === 'object' ? args[args.length-1] : {}; // opt is always the last parameter which typeof === 'object' and stands after cb
var gun = this, cat = (gun._), root = gun.back(-1);
cb = cb || noop;
opt = opt || {};

View File

@ -50,7 +50,7 @@
return; // omit!
}
}
if('~@' === soul){ // special case for shared system data, the list of aliases.
check.alias(eve, msg, val, key, soul, at, no); return;
}
@ -67,18 +67,23 @@
check.any(eve, msg, val, key, soul, at, no, at.user||''); return;
eve.to.next(msg); // not handled
}
check.hash = function(eve, msg, val, key, soul, at, no){ // mark unbuilt @i001962 's epic hex contrib!
SEA.work(val, null, function(data){
function hexToBase64(hexStr) {
let base64 = "";
for(let i = 0; i < hexStr.length; i++) {
base64 += !(i - 1 & 1) ? String.fromCharCode(parseInt(hexStr.substring(i - 1, i + 1), 16)) : ""}
return btoa(base64);}
if(data && data === key.split('#').slice(-1)[0]){ return eve.to.next(msg) }
else if (data && data === hexToBase64(key.split('#').slice(-1)[0])){
return eve.to.next(msg) }
// Verify content-addressed data matches its hash
check.hash = function (eve, msg, val, key, soul, at, no) {
function base64ToHex(data) {
var binaryStr = atob(data);
var a = [];
for (var i = 0; i < binaryStr.length; i++) {
var hex = binaryStr.charCodeAt(i).toString(16);
a.push(hex.length === 1 ? "0" + hex : hex);
}
return a.join("");
}
var hash = key.split('#').pop();
SEA.work(val, null, function (b64hash) {
var hexhash = base64ToHex(b64hash), b64slice = b64hash.slice(-20), hexslice = hexhash.slice(-20);
if ([b64hash, b64slice, hexhash, hexslice].some(item => item.endsWith(hash))) return eve.to.next(msg);
no("Data hash not same as hash!");
}, {name: 'SHA-256'});
}, { name: 'SHA-256' });
}
check.alias = function(eve, msg, val, key, soul, at, no){ // Example: {_:#~@, ~@alice: {#~@alice}}
if(!val){ return no("Data must exist!") } // data MUST exist
@ -91,7 +96,6 @@
no("Alias not same!"); // that way nobody can tamper with the list of public keys.
};
check.pub = async function(eve, msg, val, key, soul, at, no, user, pub){ var tmp // Example: {_:#~asdf, hello:'world'~fdsa}}
const raw = await S.parse(val) || {}
const verify = (certificate, certificant, cb) => {
if (certificate.m && certificate.s && certificant && pub)
// now verify certificate
@ -99,7 +103,7 @@
if (u !== data && u !== data.e && msg.put['>'] && msg.put['>'] > parseFloat(data.e)) return no("Certificate expired.") // certificate expired
// "data.c" = a list of certificants/certified users
// "data.w" = lex WRITE permission, in the future, there will be "data.r" which means lex READ permission
if (u !== data && data.c && data.w && (data.c === certificant || data.c.indexOf('*' || certificant) > -1)) {
if (u !== data && data.c && data.w && (data.c === certificant || data.c.indexOf('*') > -1 || data.c.indexOf(certificant) > -1)) {
// ok, now "certificant" is in the "certificants" list, but is "path" allowed? Check path
let path = soul.indexOf('/') > -1 ? soul.replace(soul.substring(0, soul.indexOf('/') + 1), '') : ''
String.match = String.match || Gun.text.match
@ -125,43 +129,56 @@
})
return
}
const next = () => {
JSON.stringifyAsync(msg.put[':'], function(err,s){
if(err){ return no(err || "Stringify error.") }
msg.put[':'] = s;
return eve.to.next(msg);
})
}
// Localize some opt props, and delete the original refs to prevent possible attacks
const opt = (msg._.msg || {}).opt || {}
const authenticator = opt.authenticator || (user._ || {}).sea;
const upub = opt.authenticator ? (opt.pub || (user.is || {}).pub || pub) : (user.is || {}).pub;
const cert = opt.cert;
delete opt.authenticator; delete opt.pub;
const raw = await S.parse(val) || {}
if ('pub' === key && '~' + pub === soul) {
if (val === pub) return eve.to.next(msg) // the account MUST match `pub` property that equals the ID of the public key.
return no("Account not same!")
}
if ((tmp = user.is) && tmp.pub && !raw['*'] && !raw['+'] && (pub === tmp.pub || (pub !== tmp.pub && ((msg._.msg || {}).opt || {}).cert))){
if ((user.is || authenticator) && upub && !raw['*'] && !raw['+'] && (pub === upub || (pub !== upub && cert))){
SEA.opt.pack(msg.put, packed => {
SEA.sign(packed, (user._).sea, async function(data) {
// Validate authenticator
if (!authenticator) return no("Missing authenticator");
SEA.sign(packed, authenticator, async function(data) {
if (u === data) return no(SEA.err || 'Signature fail.')
// Validate signature format
if (!data.m || !data.s) return no('Invalid signature format')
msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s}
msg.put['='] = tmp
// if writing to own graph, just allow it
if (pub === user.is.pub) {
if (pub === upub) {
if (tmp = link_is(val)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1
JSON.stringifyAsync(msg.put[':'], function(err,s){
if(err){ return no(err || "Stringify error.") }
msg.put[':'] = s;
return eve.to.next(msg);
})
next()
return
}
// if writing to other's graph, check if cert exists then try to inject cert into put, also inject self pub so that everyone can verify the put
if (pub !== user.is.pub && ((msg._.msg || {}).opt || {}).cert) {
const cert = await S.parse(msg._.msg.opt.cert)
if (pub !== upub && cert) {
const _cert = await S.parse(cert)
// even if cert exists, we must verify it
if (cert && cert.m && cert.s)
verify(cert, user.is.pub, _ => {
msg.put[':']['+'] = cert // '+' is a certificate
msg.put[':']['*'] = user.is.pub // '*' is pub of the user who puts
JSON.stringifyAsync(msg.put[':'], function(err,s){
if(err){ return no(err || "Stringify error.") }
msg.put[':'] = s;
return eve.to.next(msg);
})
if (_cert && _cert.m && _cert.s)
verify(_cert, upub, _ => {
msg.put[':']['+'] = _cert // '+' is a certificate
msg.put[':']['*'] = upub // '*' is pub of the user who puts
next()
return
})
}
@ -175,7 +192,7 @@
data = SEA.opt.unpack(data);
if (u === data) return no("Unverified data.") // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account.
if ((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1
// check if cert ('+') and putter's pub ('*') exist
if (raw['+'] && raw['+']['m'] && raw['+']['s'] && raw['*'])
// now verify certificate
@ -245,6 +262,5 @@
SEA.opt.shuffle_attack = 1546329600000; // Jan 1, 2019
var fl = Math.floor; // TODO: Still need to fix inconsistent state issue.
// TODO: Potential bug? If pub/priv key starts with `-`? IDK how possible.
}());

View File

@ -2,71 +2,168 @@
var SEA = require('./root');
var shim = require('./shim');
var S = require('./settings');
SEA.name = SEA.name || (async (cb, opt) => { try {
if(cb){ try{ cb() }catch(e){console.log(e)} }
return;
} catch(e) {
console.log(e);
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
return;
}});
// P-256 curve constants
const n = BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551");
const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b"); // Missing B parameter
const G = {
x: BigInt("0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"),
y: BigInt("0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5")
};
// Core ECC functions
function mod(a, m) { return ((a % m) + m) % m; }
// Constant-time modular inverse using Fermat's Little Theorem (p is prime)
function modInv(a, p) {
// a^(p-2) mod p
return modPow(a, p - BigInt(2), p);
}
// Constant-time modular exponentiation (square-and-multiply)
function modPow(base, exponent, modulus) {
if (modulus === BigInt(1)) return BigInt(0);
base = mod(base, modulus);
let result = BigInt(1);
while (exponent > BigInt(0)) {
if (exponent & BigInt(1)) {
result = mod(result * base, modulus);
}
exponent >>= BigInt(1);
base = mod(base * base, modulus);
}
return result;
}
// Verify a point is on the curve
function isOnCurve(point) {
if (!point) return false;
// y² = x³ + ax + b (mod p)
const { x, y } = point;
const left = mod(y * y, P);
const right = mod(mod(mod(x * x, P) * x, P) + mod(A * x, P) + B, P);
return left === right;
}
function pointAdd(p1, p2) {
if (p1 === null) return p2; if (p2 === null) return p1;
if (p1.x === p2.x && mod(p1.y + p2.y, P) === 0n) return null;
let lambda = p1.x === p2.x && p1.y === p2.y
? mod((3n * mod(p1.x ** 2n, P) + A) * modInv(2n * p1.y, P), P)
: mod((mod(p2.y - p1.y, P)) * modInv(mod(p2.x - p1.x, P), P), P);
const x3 = mod(lambda ** 2n - p1.x - p2.x, P);
return { x: x3, y: mod(lambda * mod(p1.x - x3, P) - p1.y, P) };
}
function pointMult(k, point) {
let r = null, a = point;
while (k > 0n) {
if (k & 1n) r = pointAdd(r, a);
a = pointAdd(a, a);
k >>= 1n;
}
return r;
}
//SEA.pair = async (data, proof, cb) => { try {
SEA.pair = SEA.pair || (async (cb, opt) => { try {
opt = opt || {};
const subtle = shim.subtle, ecdhSubtle = shim.ossl || subtle;
let r = {};
var ecdhSubtle = shim.ossl || shim.subtle;
// First: ECDSA keys for signing/verifying...
var sa = await shim.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, [ 'sign', 'verify' ])
.then(async (keys) => {
// privateKey scope doesn't leak out from here!
//const { d: priv } = await shim.subtle.exportKey('jwk', keys.privateKey)
var key = {};
key.priv = (await shim.subtle.exportKey('jwk', keys.privateKey)).d;
var pub = await shim.subtle.exportKey('jwk', keys.publicKey);
//const pub = Buff.from([ x, y ].join(':')).toString('base64') // old
key.pub = pub.x+'.'+pub.y; // new
// x and y are already base64
// pub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt)
// but split on a non-base64 letter.
return key;
})
// To include PGPv4 kind of keyId:
// const pubId = await SEA.keyid(keys.pub)
// Next: ECDH keys for encryption/decryption...
// Helper functions
const b64ToBI = s => {
let b64 = s.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
return BigInt('0x' + shim.Buffer.from(b64, 'base64').toString('hex'));
};
const biToB64 = n => shim.Buffer.from(n.toString(16).padStart(64, '0'), 'hex')
.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const pubFromPriv = priv => {
const pub = pointMult(priv, G);
if (!isOnCurve(pub)) throw new Error("Invalid point generated");
return biToB64(pub.x) + '.' + biToB64(pub.y);
};
const seedToKey = async (seed, salt) => {
const enc = new shim.TextEncoder();
const buf = typeof seed === 'string' ? enc.encode(seed).buffer :
seed instanceof ArrayBuffer ? seed :
seed && seed.byteLength !== undefined ? (seed.buffer || seed) : null;
if (!buf) throw new Error("Invalid seed");
const combined = new Uint8Array(buf.byteLength + enc.encode(salt).buffer.byteLength);
combined.set(new Uint8Array(buf), 0);
combined.set(new Uint8Array(enc.encode(salt).buffer), buf.byteLength);
const hash = await subtle.digest("SHA-256", combined.buffer);
let priv = BigInt("0x" + Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, "0")).join("")) % n;
if (priv <= 0n || priv >= n) priv = (priv + 1n) % n;
return priv;
};
try{
var dh = await ecdhSubtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveKey'])
.then(async (keys) => {
// privateKey scope doesn't leak out from here!
var key = {};
key.epriv = (await ecdhSubtle.exportKey('jwk', keys.privateKey)).d;
var pub = await ecdhSubtle.exportKey('jwk', keys.publicKey);
//const epub = Buff.from([ ex, ey ].join(':')).toString('base64') // old
key.epub = pub.x+'.'+pub.y; // new
// ex and ey are already base64
// epub is UTF8 but filename/URL safe (https://www.ietf.org/rfc/rfc3986.txt)
// but split on a non-base64 letter.
return key;
})
}catch(e){
if(SEA.window){ throw e }
if(e == 'Error: ECDH is not a supported algorithm'){ console.log('Ignoring ECDH...') }
else { throw e }
} dh = dh || {};
if (opt.priv) {
const priv = b64ToBI(opt.priv);
r = { priv: opt.priv, pub: pubFromPriv(priv) };
if (opt.epriv) {
r.epriv = opt.epriv;
r.epub = pubFromPriv(b64ToBI(opt.epriv));
} else {
try {
const dh = await ecdhSubtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveKey'])
.then(async k => ({
epriv: (await ecdhSubtle.exportKey('jwk', k.privateKey)).d,
epub: (await ecdhSubtle.exportKey('jwk', k.publicKey)).x + '.' +
(await ecdhSubtle.exportKey('jwk', k.publicKey)).y
}));
r.epriv = dh.epriv; r.epub = dh.epub;
} catch(e) {}
}
} else if (opt.epriv) {
r = { epriv: opt.epriv, epub: pubFromPriv(b64ToBI(opt.epriv)) };
if (opt.priv) {
r.priv = opt.priv;
r.pub = pubFromPriv(b64ToBI(opt.priv));
} else {
const sa = await subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(async k => ({
priv: (await subtle.exportKey('jwk', k.privateKey)).d,
pub: (await subtle.exportKey('jwk', k.publicKey)).x + '.' +
(await subtle.exportKey('jwk', k.publicKey)).y
}));
r.priv = sa.priv; r.pub = sa.pub;
}
} else if (opt.seed) {
const signPriv = await seedToKey(opt.seed, "-sign");
const encPriv = await seedToKey(opt.seed, "-encrypt");
r = {
priv: biToB64(signPriv), pub: pubFromPriv(signPriv),
epriv: biToB64(encPriv), epub: pubFromPriv(encPriv)
};
} else {
const sa = await subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(async k => ({
priv: (await subtle.exportKey('jwk', k.privateKey)).d,
pub: (await subtle.exportKey('jwk', k.publicKey)).x + '.' +
(await subtle.exportKey('jwk', k.publicKey)).y
}));
r = { pub: sa.pub, priv: sa.priv };
try {
const dh = await ecdhSubtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveKey'])
.then(async k => ({
epriv: (await ecdhSubtle.exportKey('jwk', k.privateKey)).d,
epub: (await ecdhSubtle.exportKey('jwk', k.publicKey)).x + '.' +
(await ecdhSubtle.exportKey('jwk', k.publicKey)).y
}));
r.epub = dh.epub; r.epriv = dh.epriv;
} catch(e) {}
}
var r = { pub: sa.pub, priv: sa.priv, /* pubId, */ epub: dh.epub, epriv: dh.epriv }
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
if(cb) try{ cb(r) }catch(e){ console.log(e) }
return r;
} catch(e) {
console.log(e);
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
if(SEA.throw) throw e;
if(cb) cb();
return;
}});

View File

@ -5,8 +5,7 @@
// IT IS IMPLEMENTED IN A POLYFILL/SHIM APPROACH.
// THIS IS AN EARLY ALPHA!
if(typeof self !== "undefined"){ module.window = self } // should be safe for at least browser/worker/nodejs, need to check other envs like RN etc.
if(typeof window !== "undefined"){ module.window = window }
module.window = (typeof globalThis !== "undefined" && typeof window === "undefined" && typeof WorkerGlobalScope !== "undefined") ? globalThis : (typeof window !== "undefined" ? window : undefined);
var tmp = module.window || module, u;
var SEA = tmp.SEA || {};

View File

@ -19,7 +19,7 @@
if(d){ jwk.d = d }
return jwk;
};
s.keyToJwk = function(keyBytes) {
const keyB64 = keyBytes.toString('base64');
const k = keyB64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');

View File

@ -6,35 +6,60 @@
var sha = require('./sha256');
var u;
async function n(r, o, c) {
try {
if(!o.raw){ r = 'SEA' + await shim.stringify(r) }
if(c){ try{ c(r) }catch(e){} }
return r;
} catch(e) { return r }
}
async function w(r, j, o, c) {
var x = {
m: j,
s: r.signature ? shim.Buffer.from(r.signature, 'binary').toString(o.encode || 'base64') : u,
a: shim.Buffer.from(r.authenticatorData, 'binary').toString('base64'),
c: shim.Buffer.from(r.clientDataJSON, 'binary').toString('base64')
};
if (!x.s || !x.a || !x.c) throw "WebAuthn signature invalid";
return n(x, o, c);
}
async function k(p, j, o, c) {
var x = S.jwk(p.pub, p.priv);
if (!x) throw "Invalid key pair";
var h = await sha(j);
var s = await (shim.ossl || shim.subtle).importKey('jwk', x, S.ecdsa.pair, false, ['sign'])
.then((k) => (shim.ossl || shim.subtle).sign(S.ecdsa.sign, k, new Uint8Array(h)))
.catch(() => { throw "SEA signature failed" });
return n({m: j, s: shim.Buffer.from(s, 'binary').toString(o.encode || 'base64')}, o, c);
}
SEA.sign = SEA.sign || (async (data, pair, cb, opt) => { try {
opt = opt || {};
if(!(pair||opt).priv){
if(!SEA.I){ throw 'No signing key.' }
if(u === data) throw '`undefined` not allowed.';
if(!(pair||opt).priv && typeof pair !== 'function'){
if(!SEA.I) throw 'No signing key.';
pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why});
}
if(u === data){ throw '`undefined` not allowed.' }
var json = await S.parse(data);
var check = opt.check = opt.check || json;
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
&& u !== await SEA.verify(check, pair)){ // don't sign if we already signed it.
var r = await S.parse(check);
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
}
var pub = pair.pub;
var priv = pair.priv;
var jwk = S.jwk(pub, priv);
var hash = await sha(json);
var sig = await (shim.ossl || shim.subtle).importKey('jwk', jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['sign'])
.then((key) => (shim.ossl || shim.subtle).sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, new Uint8Array(hash))) // privateKey scope doesn't leak out from here!
var r = {m: json, s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')}
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
var j = await S.parse(data);
var c = opt.check = opt.check || j;
if(SEA.verify && (S.check(c) || (c && c.s && c.m))
&& u !== await SEA.verify(c, pair)){
return n(await S.parse(c), opt, cb);
}
if(typeof pair === 'function') {
var r = await pair(data);
return r.authenticatorData ? w(r, j, opt, cb) :
n({m: j, s: typeof r === 'string' ? r :
r.signature && shim.Buffer.from(r.signature, 'binary').toString(opt.encode || 'base64')}, opt, cb);
}
return k(pair, j, opt, cb);
} catch(e) {
console.log(e);
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }

View File

@ -1,82 +1,153 @@
;(function(){
var SEA = require('./root');
var shim = require('./shim');
var S = require('./settings');
var sha = require('./sha256');
var u;
var SEA = require('./root');
var shim = require('./shim');
var S = require('./settings');
var sha = require('./sha256');
var u;
SEA.verify = SEA.verify || (async (data, pair, cb, opt) => { try {
var json = await S.parse(data);
if(false === pair){ // don't verify!
var raw = await S.parse(json.m);
if(cb){ try{ cb(raw) }catch(e){console.log(e)} }
return raw;
}
opt = opt || {};
// SEA.I // verify is free! Requires no user permission.
var pub = pair.pub || pair;
var key = SEA.opt.slow_leak? await SEA.opt.slow_leak(pub) : await (shim.ossl || shim.subtle).importKey('jwk', S.jwk(pub), {name: 'ECDSA', namedCurve: 'P-256'}, false, ['verify']);
var hash = await sha(json.m);
var buf, sig, check, tmp; try{
buf = shim.Buffer.from(json.s, opt.encode || 'base64'); // NEW DEFAULT!
sig = new Uint8Array(buf);
check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash));
if(!check){ throw "Signature did not match." }
}catch(e){
if(SEA.opt.fallback){
return await SEA.opt.fall_verify(data, pair, cb, opt);
}
}
var r = check? await S.parse(json.m) : u;
async function w(j, k, s) {
var a = new Uint8Array(shim.Buffer.from(j.a, 'base64'));
var c = shim.Buffer.from(j.c, 'base64').toString('utf8');
var m = new TextEncoder().encode(j.m);
var e = btoa(String.fromCharCode(...new Uint8Array(m))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
if (JSON.parse(c).challenge !== e) throw "Challenge verification failed";
var h = await (shim.ossl || shim.subtle).digest(
{name: 'SHA-256'},
new TextEncoder().encode(c)
);
var d = new Uint8Array(a.length + h.byteLength);
d.set(a);
d.set(new Uint8Array(h), a.length);
if (s[0] !== 0x30) throw "Invalid DER signature format";
var o = 2, r = new Uint8Array(64);
for(var i = 0; i < 2; i++) {
var l = s[o + 1];
o += 2;
if (s[o] === 0x00) { o++; l--; }
var p = new Uint8Array(32).fill(0);
p.set(s.slice(o, o + l), 32 - l);
r.set(p, i * 32);
o += l;
}
return (shim.ossl || shim.subtle).verify({ name: 'ECDSA', hash: {name: 'SHA-256'} }, k, r, d);
}
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
async function v(j, k, s, h) {
return (shim.ossl || shim.subtle).verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
k, s, new Uint8Array(h)
);
}
SEA.verify = SEA.verify || (async (d, p, cb, o) => { try {
var j = await S.parse(d);
if(false === p) return cb ? cb(await S.parse(j.m)) : await S.parse(j.m);
o = o || {};
var pub = p.pub || p;
var [x, y] = pub.split('.');
try {
var k = await (shim.ossl || shim.subtle).importKey('jwk', {
kty: 'EC', crv: 'P-256', x, y, ext: true, key_ops: ['verify']
}, {name: 'ECDSA', namedCurve: 'P-256'}, false, ['verify']);
var h = await sha(j.m);
var s = new Uint8Array(shim.Buffer.from(j.s || '', o.encode || 'base64'));
var c = j.a && j.c ? await w(j, k, s) : await v(j, k, s, h);
if(!c) throw "Signature did not match";
// Parse the message content
var r = await S.parse(j.m);
// Handle encrypted data consistently
// SEA encrypted data can be in two formats:
// 1. A string starting with 'SEA' followed by JSON (e.g., 'SEA{"ct":"...","iv":"...","s":"..."}')
// 2. An object with ct, iv, and s properties
// Case 1: Original message was already in SEA string format
if(typeof j.m === 'string' && j.m.startsWith('SEA{')) {
if(cb){ try{ cb(j.m) }catch(e){} }
return j.m;
}
// Case 2: Result is an encrypted data object
// This ensures consistent formatting of encrypted data as SEA strings
if(r && typeof r === 'object' &&
typeof r.ct === 'string' &&
typeof r.iv === 'string' &&
typeof r.s === 'string') {
// Format as standard SEA encrypted string
var seaStr = 'SEA' + JSON.stringify(r);
if(cb){ try{ cb(seaStr) }catch(e){} }
return seaStr;
}
// Default case: Return parsed result as is
if(cb){ try{ cb(r) }catch(e){} }
return r;
} catch(e) {
console.log(e); // mismatched owner FOR MARTTI
SEA.err = e;
if(SEA.throw){ throw e }
if(SEA.opt.fallback){
return await SEA.opt.fall_verify(d, p, cb, o);
}
if(cb){ cb() }
return;
}});
module.exports = SEA.verify;
// legacy & ossl memory leak mitigation:
var knownKeys = {};
var keyForPair = SEA.opt.slow_leak = pair => {
if (knownKeys[pair]) return knownKeys[pair];
var jwk = S.jwk(pair);
knownKeys[pair] = (shim.ossl || shim.subtle).importKey("jwk", jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ["verify"]);
return knownKeys[pair];
};
var O = SEA.opt;
SEA.opt.fall_verify = async function(data, pair, cb, opt, f){
if(f === SEA.opt.fallback){ throw "Signature did not match" } f = f || 1;
var tmp = data||'';
data = SEA.opt.unpack(data) || data;
var json = await S.parse(data), pub = pair.pub || pair, key = await SEA.opt.slow_leak(pub);
var hash = (f <= SEA.opt.fallback)? shim.Buffer.from(await shim.subtle.digest({name: 'SHA-256'}, new shim.TextEncoder().encode(await S.parse(json.m)))) : await sha(json.m); // this line is old bad buggy code but necessary for old compatibility.
var buf; var sig; var check; try{
buf = shim.Buffer.from(json.s, opt.encode || 'base64') // NEW DEFAULT!
sig = new Uint8Array(buf)
check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash))
if(!check){ throw "Signature did not match." }
}catch(e){ try{
buf = shim.Buffer.from(json.s, 'utf8') // AUTO BACKWARD OLD UTF8 DATA!
sig = new Uint8Array(buf)
check = await (shim.ossl || shim.subtle).verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, sig, new Uint8Array(hash))
}catch(e){
if(!check){ throw "Signature did not match." }
}
}
var r = check? await S.parse(json.m) : u;
O.fall_soul = tmp['#']; O.fall_key = tmp['.']; O.fall_val = data; O.fall_state = tmp['>'];
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
}
SEA.opt.fallback = 2;
} catch(e) {
SEA.err = e;
if(SEA.throw){ throw e }
if(cb){ cb() }
return;
}});
}());
module.exports = SEA.verify;
var knownKeys = {};
SEA.opt.slow_leak = pair => {
if (knownKeys[pair]) return knownKeys[pair];
var jwk = S.jwk(pair);
knownKeys[pair] = (shim.ossl || shim.subtle).importKey("jwk", jwk, {name: 'ECDSA', namedCurve: 'P-256'}, false, ["verify"]);
return knownKeys[pair];
};
SEA.opt.fall_verify = async function(data, pair, cb, opt, f){
if(f === SEA.opt.fallback){ throw "Signature did not match" }
var tmp = data||'';
data = SEA.opt.unpack(data) || data;
var json = await S.parse(data), key = await SEA.opt.slow_leak(pair.pub || pair);
var hash = (!f || f <= SEA.opt.fallback)?
shim.Buffer.from(await shim.subtle.digest({name: 'SHA-256'},
new shim.TextEncoder().encode(await S.parse(json.m)))) : await sha(json.m);
try {
var buf = shim.Buffer.from(json.s, opt.encode || 'base64');
var sig = new Uint8Array(buf);
var check = await (shim.ossl || shim.subtle).verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
key, sig, new Uint8Array(hash)
);
if(!check) throw "";
} catch(e) {
try {
buf = shim.Buffer.from(json.s, 'utf8');
sig = new Uint8Array(buf);
check = await (shim.ossl || shim.subtle).verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
key, sig, new Uint8Array(hash)
);
if(!check) throw "";
} catch(e){ throw "Signature did not match." }
}
var r = check ? await S.parse(json.m) : u;
SEA.opt.fall_soul = tmp['#']; SEA.opt.fall_key = tmp['.'];
SEA.opt.fall_val = data; SEA.opt.fall_state = tmp['>'];
if(cb){ try{ cb(r) }catch(e){console.log(e)} }
return r;
}
SEA.opt.fallback = 2;
}());

View File

@ -13,12 +13,19 @@
cb = salt;
salt = u;
}
data = (typeof data == 'string')? data : await shim.stringify(data);
// Check if data is an ArrayBuffer, if so use Uint8Array to access the data
if(data instanceof ArrayBuffer){
data = new Uint8Array(data);
data = new shim.TextDecoder("utf-8").decode(data);
}
data = (typeof data == 'string') ? data : await shim.stringify(data);
if('sha' === (opt.name||'').toLowerCase().slice(0,3)){
var rsha = shim.Buffer.from(await sha(data, opt.name), 'binary').toString(opt.encode || 'base64')
if(cb){ try{ cb(rsha) }catch(e){console.log(e)} }
return rsha;
}
if (typeof salt === "number") salt = salt.toString();
if (typeof opt.salt === "number") opt.salt = opt.salt.toString();
salt = salt || shim.random(9);
var key = await (shim.ossl || shim.subtle).importKey('raw', new shim.TextEncoder().encode(data), {name: opt.name || 'PBKDF2'}, false, ['deriveBits']);
var work = await (shim.ossl || shim.subtle).deriveBits({
@ -41,4 +48,4 @@
module.exports = SEA.work;
}());
}());

View File

@ -83,6 +83,7 @@ function list(each){ each = each || function(x){return x}
}
function set(word, is){
// TODO: Perf on random write is decent, but short keys or seq seems significantly slower.
var b = this, has = b.all[word];
if(has){ return b(word, is) } // updates to in-memory items will always match exactly.
var page = b.page(word=''+word), tmp; // before we assume this is an insert tho, we need to check
@ -103,21 +104,22 @@ function set(word, is){
function split(p, b){ // TODO: use closest hash instead of half.
//console.time();
//var S = performance.now();
var L = sort(p), l = L.length, i = l/2 >> 0, j = i, half = L[j], tmp;
//console.timeEnd();
var next = {first: half.substring(), size: 0, substring: sub, toString: to, book: b, get: b, read: list}, f = next.from = [];
//console.time();
while(tmp = L[i++]){
f.push(tmp);
next.size += (tmp.is||'').length||1;
tmp.page = next;
}
//console.timeEnd(); console.time();
p.from = p.from.slice(0, j);
p.size -= next.size;
b.list.splice(spot(next.first, b.list)+1, 0, next); // TODO: BUG! Make sure next.first is decoded text. // TODO: BUG! spot may need parse too?
//console.timeEnd();
if(b.split){ b.split(next, p) }
//console.log(S = (performance.now() - S), 'split');
//console.BIG = console.BIG > S? console.BIG : S;
}
function slot(t){ return heal((t=t||'').substring(1, t.length-1).split(t[0]), t[0]) } B.slot = slot; // TODO: check first=last & pass `s`.

View File

@ -76,10 +76,10 @@ function Mesh(root){
if((tmp = msg['><']) && 'string' == typeof tmp){ tmp.slice(0,99).split(',').forEach(function(k){ this[k] = 1 }, (msg._).yo = {}) } // Peers already sent to, do not resend.
// DAM ^
if(tmp = msg.dam){
(dup_track(id)||{}).via = peer;
if(tmp = mesh.hear[tmp]){
tmp(msg, peer, root);
}
dup_track(id);
return;
}
if(tmp = msg.ok){ msg._.near = tmp['/'] }

View File

@ -300,14 +300,11 @@ var obj_each = function(o,f){ Object.keys(o).forEach(f,o) }, text_rand = String.
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!");
}());

View File

@ -19,10 +19,10 @@ Gun.on('opt', function(root){
var mesh = opt.mesh = opt.mesh || Gun.Mesh(root);
var wire = mesh.wire || opt.wire;
var wired = mesh.wire || opt.wire;
mesh.wire = opt.wire = open;
function open(peer){ try{
if(!peer || !peer.url){ return wire && wire(peer) }
if(!peer || !peer.url){ return wired && wired(peer) }
var url = peer.url.replace(/^http/, 'ws');
var wire = peer.wire = new opt.WebSocket(url);
wire.onclose = function(){

View File

@ -1,3 +1,7 @@
const exp = require('constants');
const expect = require('../expect');
const SeaArray = require('../../sea/array.js');
var root;
var Gun;
(function(){
@ -196,6 +200,30 @@ describe('SEA', function(){
done();
});});});
})*/
it('hash array buffer', function(done) {
(async function() {
// Create a random ArrayBuffer (buffer 1)
var buff1 = new ArrayBuffer(16);
var view1 = new Uint8Array(buff1); // Use a Uint8Array to modify the buffer
for (var i = 0; i < view1.length; i++) {
view1[i] = Math.floor(Math.random() * 256);
}
var hash1 = await SEA.work(buff1, "salt");
// Create another random ArrayBuffer (buffer 2)
var buff2 = new ArrayBuffer(16);
var view2 = new Uint8Array(buff2);
for (var i = 0; i < view2.length; i++) {
view2[i] = Math.floor(Math.random() * 256);
}
var hash2 = await SEA.work(buff2, "salt");
// Ensure the hashes are strings and different from each other
expect(typeof hash1 === "string" && typeof hash2 === "string" && hash1 !== hash2).to.be(true);
done(); // Signal that the test is complete
})();
});
it('legacy', function(done){ (async function(){
var pw = 'test123';
@ -235,7 +263,7 @@ describe('SEA', function(){
var alias = SEA.opt.unpack(await SEA.verify(old.alias, false), 'alias', old);
expect(alias).to.be('alice');
alias = Gun.state.ify({}, tmp, 1, {'#': tmp}, tmp = '~@'+alias);
gun._.graph[tmp] = graph[tmp] = alias;
gun._.graph[tmp] = alias;
//gun.on('test', {$: gun, put: graph});
var use = gun.user();
use.auth('alice', 'test123', function(ack){
@ -287,9 +315,317 @@ describe('SEA', function(){
}())})
});
describe('Seed-based Key Generation', function() {
this.timeout(5000); // Set timeout for all tests in this suite
it('generates deterministic key pairs from same seed', async function () {
// Seed string tests
const pair1 = await SEA.pair(null, { seed: "my secret seed" });
const pair2 = await SEA.pair(null, { seed: "my secret seed" });
const pair3 = await SEA.pair(null, { seed: "not my seed" });
// Check if pairs with same seed are identical
const sameKeys = pair1.priv === pair2.priv &&
pair1.pub === pair2.pub &&
pair1.epriv === pair2.epriv &&
pair1.epub === pair2.epub;
// Check if pairs with different seeds are different
const differentKeys = pair1.priv !== pair3.priv &&
pair1.pub !== pair3.pub &&
pair1.epriv !== pair3.epriv &&
pair1.epub !== pair3.epub;
expect(sameKeys).to.be(true);
expect(differentKeys).to.be(true);
// Test consistent generation across multiple calls
const numTests = 5;
const pairs = [];
const seed = "consistency test seed";
// Generate multiple pairs with the same seed
for (let i = 0; i < numTests; i++) {
pairs.push(await SEA.pair(null, { seed }));
}
// Verify all pairs are identical
let allMatch = true;
for (let i = 1; i < numTests; i++) {
if (pairs[i].pub !== pairs[0].pub ||
pairs[i].priv !== pairs[0].priv ||
pairs[i].epub !== pairs[0].epub ||
pairs[i].epriv !== pairs[0].epriv) {
allMatch = false;
break;
}
}
expect(allMatch).to.be(true);
// Test that the created pair works with SEA functions
var enc = await SEA.encrypt('hello self', pair1);
var data = await SEA.sign(enc, pair1);
var msg = await SEA.verify(data, pair1.pub);
expect(msg).to.be(enc);
var dec = await SEA.decrypt(msg, pair1);
expect(dec).to.be('hello self');
var proof = await SEA.work(dec, pair1);
var check = await SEA.work('hello self', pair1);
expect(proof).to.be(check);
});
it('generates deterministic key pairs from ArrayBuffer seed', async function () {
// Create ArrayBuffer seeds
const textEncoder = new TextEncoder();
const seedData1 = textEncoder.encode("my secret seed"); // Convert string to Uint8Array
const seedBuffer1 = seedData1.buffer; // Get the underlying ArrayBuffer
// Create a second identical seed
const seedData2 = textEncoder.encode("my secret seed");
const seedBuffer2 = seedData2.buffer;
// Create a different seed
const seedData3 = textEncoder.encode("not my seed");
const seedBuffer3 = seedData3.buffer;
// Generate key pairs using ArrayBuffer seeds
const pair1 = await SEA.pair(null, { seed: seedBuffer1 });
const pair2 = await SEA.pair(null, { seed: seedBuffer2 });
const pair3 = await SEA.pair(null, { seed: seedBuffer3 });
// Check if pairs with same seed content are identical
const sameKeys = pair1.priv === pair2.priv &&
pair1.pub === pair2.pub &&
pair1.epriv === pair2.epriv &&
pair1.epub === pair2.epub;
// Check if pairs with different seeds are different
const differentKeys = pair1.priv !== pair3.priv &&
pair1.pub !== pair3.pub &&
pair1.epriv !== pair3.epriv &&
pair1.epub !== pair3.epub;
expect(sameKeys).to.be(true);
expect(differentKeys).to.be(true);
// Test with different ways to create ArrayBuffer seeds
// Method 1: Direct encoding
const buffer1 = textEncoder.encode("buffer-seed-test").buffer;
// Method 2: Clone buffer from another array
const tempArray = textEncoder.encode("buffer-seed-test");
const buffer2 = tempArray.buffer.slice(0);
// Generate key pairs
const bufPair1 = await SEA.pair(null, { seed: buffer1 });
const bufPair2 = await SEA.pair(null, { seed: buffer2 });
// Keys should be identical
expect(bufPair1.pub).to.be(bufPair2.pub);
expect(bufPair1.priv).to.be(bufPair2.priv);
expect(bufPair1.epub).to.be(bufPair2.epub);
expect(bufPair1.epriv).to.be(bufPair2.epriv);
// Test that different buffers produce different keys
const buffer3 = textEncoder.encode("different-buffer-seed").buffer;
const bufPair3 = await SEA.pair(null, { seed: buffer3 });
expect(bufPair1.pub).to.not.be(bufPair3.pub);
// Test that the created pair works with SEA functions
var enc = await SEA.encrypt('hello self', bufPair1);
var data = await SEA.sign(enc, bufPair1);
var msg = await SEA.verify(data, bufPair1.pub);
expect(msg).to.be(enc);
var dec = await SEA.decrypt(msg, bufPair1);
expect(dec).to.be('hello self');
var proof = await SEA.work(dec, bufPair1);
var check = await SEA.work('hello self', bufPair1);
expect(proof).to.be(check);
});
it('generate key pairs from private key', async function () {
var gun = Gun()
var user = gun.user()
const test1 = await SEA.pair(null, { seed: "seed" });
const test2 = await SEA.pair(null, { priv: test1.priv });
expect(test2.priv).to.be(test1.priv);
expect(test2.pub).to.be(test1.pub);
// Test that the created pair works with SEA functions
var enc = await SEA.encrypt('hello self', test2);
var data = await SEA.sign(enc, test2);
var msg = await SEA.verify(data, test2.pub);
expect(msg).to.be(enc);
var dec = await SEA.decrypt(msg, test2);
expect(dec).to.be('hello self');
var proof = await SEA.work(dec, test2);
var check = await SEA.work('hello self', test2);
expect(proof).to.be(check);
await user.auth(test2);
expect(user.is.pub).to.be(test2.pub);
expect(user.is.pub).to.be(test1.pub);
user.leave();
const test3 = await SEA.pair(null, { epriv: test2.epriv });
expect(test3.epriv).to.be(test2.epriv);
await user.auth(test3);
expect(user.is.epub).to.be(test3.epub);
expect(user.is.epub).to.be(test2.epub);
user.leave();
});
it('handles different types of seed values correctly', async function () {
// Test different seed types
const testCases = [
{ type: "empty string", seed: "" },
{ type: "numeric", seed: "12345" },
{ type: "special chars", seed: "!@#$%^&*()" },
{ type: "long string", seed: "a".repeat(1000) },
{ type: "unicode", seed: "😀🔑🔒👍" }
];
// Generate pairs for each test case
const results = [];
for (const test of testCases) {
try {
const pair = await SEA.pair(null, { seed: test.seed });
// Check if pair has all required properties
const isValid = pair &&
typeof pair.pub === 'string' &&
typeof pair.priv === 'string' &&
typeof pair.epub === 'string' &&
typeof pair.epriv === 'string';
results.push({ ...test, success: isValid, pair: pair });
} catch (e) {
results.push({ ...test, success: false, error: e.message });
}
}
// All test cases should succeed
const allSucceeded = results.every(r => r.success);
expect(allSucceeded).to.be(true);
// All pairs should be different from each other
const uniquePairs = new Set(results.map(r => r.pair?.pub));
expect(uniquePairs.size).to.be(results.length);
// Similar seeds should produce different key pairs
const seed1 = "test-seed";
const seed2 = "test-seed1";
const seed3 = "test-seed "; // note the space
const seed4 = "Test-seed"; // capitalization
const pairs = await Promise.all([
SEA.pair(null, { seed: seed1 }),
SEA.pair(null, { seed: seed2 }),
SEA.pair(null, { seed: seed3 }),
SEA.pair(null, { seed: seed4 })
]);
// Check that all pairs are different
const [p1, p2, p3, p4] = pairs;
expect(p1.pub).to.not.equal(p2.pub);
expect(p1.pub).to.not.equal(p3.pub);
expect(p1.pub).to.not.equal(p4.pub);
expect(p2.pub).to.not.equal(p3.pub);
expect(p2.pub).to.not.equal(p4.pub);
expect(p3.pub).to.not.equal(p4.pub);
});
it('works with SEA operations (sign, verify, encrypt, decrypt)', async function () {
// Test with sign/verify
const seed = "sign-verify-seed";
const pair = await SEA.pair(null, { seed });
const message = "Hello deterministic world!";
// Test signing and verification
const signature = await SEA.sign(message, pair);
const verified = await SEA.verify(signature, pair.pub);
expect(verified).to.be(message);
// Test with encrypt/decrypt
const encryptSeed = "encrypt-decrypt-seed";
const encPair = await SEA.pair(null, { seed: encryptSeed });
const secretMessage = "Secret deterministic message";
// Test encryption and decryption
const encrypted = await SEA.encrypt(secretMessage, encPair);
const decrypted = await SEA.decrypt(encrypted, encPair);
expect(decrypted).to.be(secretMessage);
// Test with SEA.secret (key exchange)
const aliceSeed = "alice-deterministic";
const bobSeed = "bob-deterministic";
const alice = await SEA.pair(null, { seed: aliceSeed });
const bob = await SEA.pair(null, { seed: bobSeed });
// Generate shared secrets
const aliceShared = await SEA.secret(bob.epub, alice);
const bobShared = await SEA.secret(alice.epub, bob);
expect(aliceShared).to.be(bobShared);
// Test shared secret for encryption
const sharedMessage = "Secret shared deterministically";
const sharedEncrypted = await SEA.encrypt(sharedMessage, aliceShared);
const sharedDecrypted = await SEA.decrypt(sharedEncrypted, bobShared);
expect(sharedDecrypted).to.be(sharedMessage);
// Test complete workflow
const workflowSeed = "workflow-test-seed";
const workflowPair = await SEA.pair(null, { seed: workflowSeed });
const workflowMessage = "hello deterministic self";
// Complete workflow: encrypt, sign, verify, decrypt
const wfEncrypted = await SEA.encrypt(workflowMessage, workflowPair);
const wfSigned = await SEA.sign(wfEncrypted, workflowPair);
const wfVerified = await SEA.verify(wfSigned, workflowPair.pub);
const wfDecrypted = await SEA.decrypt(wfVerified, workflowPair);
expect(wfDecrypted).to.be(workflowMessage);
// Test with SEA.work
const proof1 = await SEA.work(workflowMessage, workflowPair);
const proof2 = await SEA.work(workflowMessage, workflowPair);
expect(proof1).to.be(proof2);
});
});
describe('User', function(){
var gun = Gun(), gtmp;
it("put to user graph without having to be authenticated (provide pair)", function(done){(async function(){
var bob = await SEA.pair();
gun.get(`~${bob.pub}`).get('test').put('this is Bob', (ack) => {
gun.get(`~${bob.pub}`).get('test').once((data) => {
expect(ack.err).to.not.be.ok()
expect(data).to.be('this is Bob')
done();
})
}, {opt: {authenticator: bob}})
})()});
it("put to user graph using external authenticator (nested SEA.sign)", function(done){(async function(){
var bob = await SEA.pair();
async function authenticator(data) {
const sig = await SEA.sign(data, bob)
return sig
}
gun.get(`~${bob.pub}`).get('test').put('this is Bob', (ack) => {
gun.get(`~${bob.pub}`).get('test').once((data) => {
expect(ack.err).to.not.be.ok()
expect(data).to.be('this is Bob')
done();
})
}, {opt: {authenticator: authenticator}})
})()});
it('test', function(done){
var g = Gun();
user = g.user();
@ -750,7 +1086,7 @@ describe('SEA', function(){
});
describe.skip('Frozen', function () {
describe('Frozen', function () {
it('Across spaces', function(done){
var gun = Gun();
var user = gun.user();
@ -763,15 +1099,14 @@ describe('SEA', function(){
var data = "hello world";
var hash = await SEA.work(data, null, null, {name: "SHA-256"});
gun.get('#users').get(hash).put(data);
console.log(1);
gun.get('#users').map()/*.get('country')*/.on(data => console.log(data));
hash = hash.slice(-20);
await gun.get('#users').get(hash).put(data);
var test = await gun.get('#users').get(hash);
expect(test).to.be(data);
done();
});
});
});
})
}());