mirror of
https://github.com/amark/gun.git
synced 2025-03-30 15:08:33 +00:00
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:
parent
ff4bf9293c
commit
f0cce073a8
20
examples/webauthn.html
Normal file
20
examples/webauthn.html
Normal 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
176
examples/webauthn.js
Normal 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
5
gun.js
@ -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
2
gun.min.js
vendored
File diff suppressed because one or more lines are too long
138
lib/build.js
Normal file
138
lib/build.js
Normal 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;
|
@ -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
1189
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
559
sea.js
@ -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');
|
||||
}());
|
||||
|
||||
}());
|
@ -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'])
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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 = []
|
||||
|
@ -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 || {};
|
||||
|
92
sea/index.js
92
sea/index.js
@ -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.
|
||||
|
||||
|
||||
}());
|
209
sea/pair.js
209
sea/pair.js
@ -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;
|
||||
}});
|
||||
|
||||
|
@ -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 || {};
|
||||
|
@ -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, '');
|
||||
|
71
sea/sign.js
71
sea/sign.js
@ -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() }
|
||||
|
215
sea/verify.js
215
sea/verify.js
@ -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;
|
||||
|
||||
}());
|
||||
|
11
sea/work.js
11
sea/work.js
@ -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;
|
||||
|
||||
}());
|
||||
}());
|
||||
|
@ -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`.
|
||||
|
@ -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['/'] }
|
||||
|
@ -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!");
|
||||
|
||||
}());
|
@ -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(){
|
||||
|
351
test/sea/sea.js
351
test/sea/sea.js
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
}());
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user