mirror of
https://github.com/amark/gun.git
synced 2025-03-30 15:08:33 +00:00
feat: enhance WebAuthn integration with new put options and improved signature handling
This commit is contained in:
parent
374fd592d8
commit
e2820a1888
@ -13,7 +13,8 @@
|
||||
<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</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>
|
@ -72,12 +72,16 @@ document.getElementById('create').onclick = async () => {
|
||||
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 },
|
||||
{ type: "public-key", alg: -257 },
|
||||
{ type: "public-key", alg: -37 }
|
||||
{ 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" },
|
||||
authenticatorSelection: {
|
||||
userVerification: "preferred"
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: "none"
|
||||
}
|
||||
@ -118,9 +122,9 @@ const authenticator = async (data) => {
|
||||
timeout: 60000
|
||||
}
|
||||
};
|
||||
console.log("Auth options:", options);
|
||||
|
||||
const assertion = await navigator.credentials.get(options);
|
||||
console.log("SIGNED:", {options, assertion});
|
||||
return assertion.response;
|
||||
};
|
||||
|
||||
@ -159,4 +163,14 @@ document.getElementById('put').onclick = async () => {
|
||||
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)
|
||||
}
|
105
sea.js
105
sea.js
@ -388,58 +388,81 @@
|
||||
opt = opt || {};
|
||||
|
||||
// Format and return the final response
|
||||
async function next(r, opt, cb) {
|
||||
try {
|
||||
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
|
||||
if(cb){ try{ cb(r) }catch(e){} }
|
||||
return r;
|
||||
} catch(e) {
|
||||
console.warn('SEA.sign response error', e);
|
||||
return r;
|
||||
}
|
||||
async function next(r) {
|
||||
try {
|
||||
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
|
||||
if(cb){ try{ cb(r) }catch(e){} }
|
||||
return r;
|
||||
} catch(e) {
|
||||
console.warn('SEA.sign response error', e);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
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});
|
||||
// WebAuthn
|
||||
async function wa(res, json) {
|
||||
var r = {
|
||||
m: json,
|
||||
s: res.signature ? shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64') : undefined,
|
||||
a: shim.Buffer.from(res.authenticatorData, 'binary').toString('base64'),
|
||||
c: shim.Buffer.from(res.clientDataJSON, 'binary').toString('base64')
|
||||
};
|
||||
if (!r.s || !r.a || !r.c) throw "WebAuthn signature invalid";
|
||||
return next(r);
|
||||
}
|
||||
|
||||
// External auth fn
|
||||
async function ea(res, json) {
|
||||
if (!res) throw new Error('Empty auth response');
|
||||
if (typeof res === 'string') {
|
||||
return next({ m: json, s: res });
|
||||
}
|
||||
|
||||
var json = await S.parse(data);
|
||||
var check = opt.check = opt.check || json;
|
||||
|
||||
// Return early if already signed
|
||||
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
|
||||
&& u !== await SEA.verify(check, pair)){
|
||||
return next(await S.parse(check), opt, cb);
|
||||
}
|
||||
|
||||
// Handle WebAuthn
|
||||
if(typeof pair === 'function'){
|
||||
const response = await pair(data);
|
||||
var r = {
|
||||
if (res.signature) {
|
||||
return next({
|
||||
m: json,
|
||||
s: response.signature ? shim.Buffer.from(response.signature, 'binary').toString(opt.encode || 'base64') : undefined,
|
||||
a: response.authenticatorData ? shim.Buffer.from(response.authenticatorData, 'binary').toString('base64') : undefined,
|
||||
c: response.clientDataJSON ? shim.Buffer.from(response.clientDataJSON, 'binary').toString('base64') : undefined
|
||||
};
|
||||
if (!r.s || !r.a || !r.c) { throw "WebAuthn signature invalid"; }
|
||||
return next(r, opt, cb);
|
||||
s: shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64')
|
||||
});
|
||||
}
|
||||
throw new Error('Invalid auth format');
|
||||
}
|
||||
|
||||
// Handle regular signing
|
||||
// Key pair
|
||||
async function kp(pair, json) {
|
||||
var jwk = S.jwk(pair.pub, pair.priv);
|
||||
if (!jwk) throw new Error('Invalid key pair');
|
||||
|
||||
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)))
|
||||
.catch(e => { throw new Error('SEA signature failed: ' + e.message) });
|
||||
|
||||
return next({
|
||||
m: json,
|
||||
m: json,
|
||||
s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')
|
||||
}, opt, cb);
|
||||
});
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
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)){
|
||||
return next(await S.parse(check));
|
||||
}
|
||||
|
||||
if(typeof pair === 'function') {
|
||||
const response = await pair(data);
|
||||
const fn = response.authenticatorData ? wa : ea;
|
||||
return fn(response, json);
|
||||
}
|
||||
|
||||
return kp(pair, json);
|
||||
} catch(e) {
|
||||
SEA.err = e;
|
||||
if(SEA.throw){ throw e }
|
||||
@ -1537,10 +1560,16 @@
|
||||
const raw = await S.parse(val) || {}
|
||||
if ((user.is || opt.authenticator) && (tmp = opt.authenticator ? (opt.pub || (user.is || {}).pub || pub) : (user.is || {}).pub) && tmp && !raw['*'] && !raw['+'] && (pub === tmp || (pub !== tmp && opt.cert))){
|
||||
SEA.opt.pack(msg.put, packed => {
|
||||
const authenticator = opt.authenticator || (user._).sea;
|
||||
// Determine the authenticator to use - external or user's
|
||||
const authenticator = typeof opt.authenticator === 'function' ? opt.authenticator : (opt.authenticator || (user._).sea);
|
||||
const upub = tmp;
|
||||
// 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
|
||||
|
||||
|
10
sea/index.js
10
sea/index.js
@ -147,10 +147,16 @@
|
||||
const raw = await S.parse(val) || {}
|
||||
if ((user.is || opt.authenticator) && (tmp = opt.authenticator ? (opt.pub || (user.is || {}).pub || pub) : (user.is || {}).pub) && tmp && !raw['*'] && !raw['+'] && (pub === tmp || (pub !== tmp && opt.cert))){
|
||||
SEA.opt.pack(msg.put, packed => {
|
||||
const authenticator = opt.authenticator || (user._).sea;
|
||||
// Determine the authenticator to use - external or user's
|
||||
const authenticator = typeof opt.authenticator === 'function' ? opt.authenticator : (opt.authenticator || (user._).sea);
|
||||
const upub = tmp;
|
||||
// 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
|
||||
|
||||
@ -254,4 +260,4 @@
|
||||
var fl = Math.floor; // TODO: Still need to fix inconsistent state issue.
|
||||
// TODO: Potential bug? If pub/priv key starts with `-`? IDK how possible.
|
||||
|
||||
}());
|
||||
}());
|
||||
|
83
sea/sign.js
83
sea/sign.js
@ -9,7 +9,7 @@
|
||||
opt = opt || {};
|
||||
|
||||
// Format and return the final response
|
||||
async function next(r, opt, cb) {
|
||||
async function next(r) {
|
||||
try {
|
||||
if(!opt.raw){ r = 'SEA' + await shim.stringify(r) }
|
||||
if(cb){ try{ cb(r) }catch(e){} }
|
||||
@ -19,48 +19,71 @@
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if(u === data){ throw '`undefined` not allowed.' }
|
||||
|
||||
// WebAuthn
|
||||
async function wa(res, json) {
|
||||
var r = {
|
||||
m: json,
|
||||
s: res.signature ? shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64') : undefined,
|
||||
a: shim.Buffer.from(res.authenticatorData, 'binary').toString('base64'),
|
||||
c: shim.Buffer.from(res.clientDataJSON, 'binary').toString('base64')
|
||||
};
|
||||
if (!r.s || !r.a || !r.c) throw "WebAuthn signature invalid";
|
||||
return next(r);
|
||||
}
|
||||
|
||||
// External auth fn
|
||||
async function ea(res, json) {
|
||||
if (!res) throw new Error('Empty auth response');
|
||||
if (typeof res === 'string') {
|
||||
return next({ m: json, s: res });
|
||||
}
|
||||
if (res.signature) {
|
||||
return next({
|
||||
m: json,
|
||||
s: shim.Buffer.from(res.signature, 'binary').toString(opt.encode || 'base64')
|
||||
});
|
||||
}
|
||||
throw new Error('Invalid auth format');
|
||||
}
|
||||
|
||||
// Key pair
|
||||
async function kp(pair, json) {
|
||||
var jwk = S.jwk(pair.pub, pair.priv);
|
||||
if (!jwk) throw new Error('Invalid key pair');
|
||||
|
||||
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)))
|
||||
.catch(e => { throw new Error('SEA signature failed: ' + e.message) });
|
||||
|
||||
return next({
|
||||
m: json,
|
||||
s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')
|
||||
});
|
||||
}
|
||||
|
||||
if(u === data) throw '`undefined` not allowed.';
|
||||
if(!(pair||opt).priv && typeof pair !== 'function'){
|
||||
if(!SEA.I){ throw 'No signing key.' }
|
||||
if(!SEA.I) throw 'No signing key.';
|
||||
pair = await SEA.I(null, {what: data, how: 'sign', why: opt.why});
|
||||
}
|
||||
|
||||
var json = await S.parse(data);
|
||||
var check = opt.check = opt.check || json;
|
||||
|
||||
// Return early if already signed
|
||||
if(SEA.verify && (SEA.opt.check(check) || (check && check.s && check.m))
|
||||
&& u !== await SEA.verify(check, pair)){
|
||||
return next(await S.parse(check), opt, cb);
|
||||
return next(await S.parse(check));
|
||||
}
|
||||
|
||||
// Handle WebAuthn
|
||||
if(typeof pair === 'function'){
|
||||
if(typeof pair === 'function') {
|
||||
const response = await pair(data);
|
||||
var r = {
|
||||
m: json,
|
||||
s: response.signature ? shim.Buffer.from(response.signature, 'binary').toString(opt.encode || 'base64') : undefined,
|
||||
a: response.authenticatorData ? shim.Buffer.from(response.authenticatorData, 'binary').toString('base64') : undefined,
|
||||
c: response.clientDataJSON ? shim.Buffer.from(response.clientDataJSON, 'binary').toString('base64') : undefined
|
||||
};
|
||||
if (!r.s || !r.a || !r.c) { throw "WebAuthn signature invalid"; }
|
||||
return next(r, opt, cb);
|
||||
const fn = response.authenticatorData ? wa : ea;
|
||||
return fn(response, json);
|
||||
}
|
||||
|
||||
// Handle regular signing
|
||||
var jwk = S.jwk(pair.pub, pair.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)))
|
||||
.catch(e => { throw new Error('SEA signature failed: ' + e.message) });
|
||||
|
||||
return next({
|
||||
m: json,
|
||||
s: shim.Buffer.from(sig, 'binary').toString(opt.encode || 'base64')
|
||||
}, opt, cb);
|
||||
|
||||
|
||||
return kp(pair, json);
|
||||
} catch(e) {
|
||||
SEA.err = e;
|
||||
if(SEA.throw){ throw e }
|
||||
|
@ -328,6 +328,32 @@ describe('SEA', function(){
|
||||
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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user