diff --git a/doc/api/SubtleCrypto.json b/doc/api/SubtleCrypto.json index e56a4a56..85c24132 100644 --- a/doc/api/SubtleCrypto.json +++ b/doc/api/SubtleCrypto.json @@ -287,8 +287,7 @@ "union": [ "'spki'", "'pkcs8'", - "'raw'", - "'teeKeyHandle'" + "'raw'" ] } }, @@ -346,7 +345,7 @@ } }, "generateKey": { - "description": "Generates new keys. Currently only supports the Elliptic Curve Diffie-Hellman (ECDH) algorithm to generate key pairs.", + "description": "Generates new keys. Currently only supports the Elliptic Curve Diffie-Hellman (ECDH) and Elliptic Curve Digital Signature Algorithm (ECDSA) algorithms to generate key pairs. When `extractable` is set to `true`, the raw key material can be exported using `exportKey`. When `extractable` is set to `false`, for ECDSA and ECDH keys `exportKey` returns an opaque handle to the key in the device's trusted execution environment, and throws for other key formats.", "parameters": [ { "name": "algorithm", @@ -376,7 +375,6 @@ "optional": true, "type": { "map": { - "inTee": { "type": "boolean", "optional": true }, "usageRequiresAuth": { "type": "boolean", "optional": true } } } @@ -491,15 +489,14 @@ } }, "exportKey": { - "description": "Converts a CryptoKey instances into a portable format. To export a key, the key must have extractable set to true. Supports the spki format or raw bytes.", + "description": "Converts `CryptoKey` instances into a portable format. If the key's `extractable` is set to `true`, returns the raw key material in SPKI format or as raw bytes. If the key's `extractable` is set to `false`, for ECDSA and ECDH keys returns an opaque handle to the key in the device's trusted execution environment, and throws for other key formats.", "parameters": [ { "name": "format", "type": { "union": [ "'raw'", - "'spki'", - "'teeKeyHandle'" + "'spki'" ] } }, diff --git a/snippets/crypto-derive.ts b/snippets/crypto-derive.ts index 4710a673..4c2e4841 100644 --- a/snippets/crypto-derive.ts +++ b/snippets/crypto-derive.ts @@ -8,7 +8,7 @@ tabris.onLog(({message}) => stack.append(TextView({text: message}))); (async () => { await importAndDerive(); await generateDeriveEncryptAndDecrypt(); - await generateDeriveEncryptAndDecrypt({inTee: true, usageRequiresAuth: true}); + await generateDeriveEncryptAndDecrypt({extractable: false, usageRequiresAuth: true}); })().catch(console.error); async function importAndDerive() { @@ -103,7 +103,10 @@ async function importAndDerive() { } } -async function generateDeriveEncryptAndDecrypt({inTee, usageRequiresAuth} = {inTee: false, usageRequiresAuth: false}) { +async function generateDeriveEncryptAndDecrypt({extractable, usageRequiresAuth} = { + extractable: true, + usageRequiresAuth: false +}) { const ecdhP256 = {name: 'ECDH' as const, namedCurve: 'P-256' as const}; const aesGcm = {name: 'AES-GCM' as const}; @@ -111,7 +114,7 @@ async function generateDeriveEncryptAndDecrypt({inTee, usageRequiresAuth} = {inT const alicesKeyPair = await crypto.subtle.generateKey(ecdhP256, true, ['deriveBits']); // Generate Bob's ECDH key pair - const bobsKeyPair = await crypto.subtle.generateKey(ecdhP256, true, ['deriveBits'], {inTee, usageRequiresAuth}); + const bobsKeyPair = await crypto.subtle.generateKey(ecdhP256, extractable, ['deriveBits'], {usageRequiresAuth}); // Derive Alice's AES key const alicesAesKey = await deriveAesKey(bobsKeyPair.publicKey, alicesKeyPair.privateKey, 'encrypt'); diff --git a/snippets/crypto-sign.ts b/snippets/crypto-sign.ts index 97889c1c..889a67d6 100644 --- a/snippets/crypto-sign.ts +++ b/snippets/crypto-sign.ts @@ -6,10 +6,10 @@ tabris.onLog(({message}) => stack.append(TextView({text: message}))); (async function() { await signAndVerify(); - await signAndVerify({inTee: true, usageRequiresAuth: true}); + await signAndVerify({extractable: false, usageRequiresAuth: true}); }()).catch(console.error); -async function signAndVerify({inTee, usageRequiresAuth} = {inTee: false, usageRequiresAuth: false}) { +async function signAndVerify({extractable, usageRequiresAuth} = {extractable: true, usageRequiresAuth: false}) { console.log('ECDSA signing/verification with generated keys:'); const generationAlgorithm = {name: 'ECDSA' as const, namedCurve: 'P-256' as const}; const signingAlgorithm = {name: 'ECDSAinDERFormat' as const, hash: 'SHA-256' as const}; @@ -17,26 +17,26 @@ async function signAndVerify({inTee, usageRequiresAuth} = {inTee: false, usageRe // Generate a key pair for signing and verifying const keyPair = await crypto.subtle.generateKey( generationAlgorithm, - true, + extractable, ['sign', 'verify'], - {inTee, usageRequiresAuth} + {usageRequiresAuth} ); let privateKeyImportedFromTee: CryptoKey; - if (inTee) { - // Export the private key and import it back - const privateKeyHandle = await crypto.subtle.exportKey('teeKeyHandle', keyPair.privateKey); + if (!extractable) { + // Export a handle of the private key stored in the Trusted Execution Environment and import it back + const privateKeyHandle = await crypto.subtle.exportKey('raw', keyPair.privateKey); const alg = {name: 'ECDSA' as const, namedCurve: 'P-256' as const}; - privateKeyImportedFromTee = await crypto.subtle.importKey('teeKeyHandle', privateKeyHandle, alg, true, ['sign']); + privateKeyImportedFromTee = await crypto.subtle.importKey('raw', privateKeyHandle, alg, extractable, ['sign']); } // Export the public key and import it back const publicKeySpki = await crypto.subtle.exportKey('spki', keyPair.publicKey); - const publicKey = await crypto.subtle.importKey('spki', publicKeySpki, generationAlgorithm, true, ['verify']); + const publicKey = await crypto.subtle.importKey('spki', publicKeySpki, generationAlgorithm, extractable, ['verify']); // Sign a message const message = await new Blob(['Message']).arrayBuffer(); - const privateKey = inTee ? privateKeyImportedFromTee : keyPair.privateKey; + const privateKey = !extractable ? privateKeyImportedFromTee : keyPair.privateKey; const signature = await crypto.subtle.sign(signingAlgorithm, privateKey, message); console.log('Signature:', new Uint8Array(signature).join(', ')); diff --git a/src/tabris/Crypto.ts b/src/tabris/Crypto.ts index 1a702b8d..5d334a7e 100644 --- a/src/tabris/Crypto.ts +++ b/src/tabris/Crypto.ts @@ -83,7 +83,7 @@ class SubtleCrypto { if (arguments.length !== 5) { throw new TypeError(`Expected 5 arguments, got ${arguments.length}`); } - allowOnlyValues(format, ['spki', 'pkcs8', 'raw', 'teeKeyHandle'], 'format'); + allowOnlyValues(format, ['spki', 'pkcs8', 'raw'], 'format'); checkType(getBuffer(keyData), ArrayBuffer, {name: 'keyData'}); if (typeof algorithm === 'string') { allowOnlyValues(algorithm, ['AES-GCM', 'HKDF'], 'algorithm'); @@ -220,13 +220,13 @@ class SubtleCrypto { } async exportKey( - format: 'raw' | 'spki' | 'teeKeyHandle', + format: 'raw' | 'spki', key: CryptoKey ): Promise { if (arguments.length !== 2) { throw new TypeError(`Expected 2 arguments, got ${arguments.length}`); } - allowOnlyValues(format, ['raw', 'spki', 'teeKeyHandle'], 'format'); + allowOnlyValues(format, ['raw', 'spki'], 'format'); checkType(key, CryptoKey, {name: 'key'}); return new Promise((onSuccess, onReject) => this._nativeObject.subtleExportKey(format, key, onSuccess, onReject) @@ -248,21 +248,17 @@ class SubtleCrypto { checkType(extractable, Boolean, {name: 'extractable'}); checkType(keyUsages, Array, {name: 'keyUsages'}); if (options != null) { - allowOnlyKeys(options, ['inTee', 'usageRequiresAuth']); - if('inTee' in options) { - checkType(options.inTee, Boolean, {name: 'options.inTee'}); - } + allowOnlyKeys(options, ['usageRequiresAuth']); if ('usageRequiresAuth' in options) { checkType(options.usageRequiresAuth, Boolean, {name: 'options.usageRequiresAuth'}); } - if (options.usageRequiresAuth && !options.inTee && (tabris as any).device.platform !== 'Android') { - throw new TypeError('options.usageRequiresAuth is only supported for keys not in TEE on Android'); + if (options.usageRequiresAuth && (extractable || !algorithm.name.startsWith('EC'))) { + throw new TypeError('options.usageRequiresAuth is only supported for non-extractable EC keys'); } } - const inTee = options?.inTee; const usageRequiresAuth = options?.usageRequiresAuth; const nativeObject = new _CryptoKey(); - await nativeObject.generate(algorithm, extractable, keyUsages, inTee, usageRequiresAuth); + await nativeObject.generate(algorithm, extractable, keyUsages, usageRequiresAuth); const nativePrivate = new _CryptoKey(nativeObject, 'private'); const nativePublic = new _CryptoKey(nativeObject, 'public'); return { diff --git a/src/tabris/CryptoKey.ts b/src/tabris/CryptoKey.ts index 6d577225..6f8df9ed 100644 --- a/src/tabris/CryptoKey.ts +++ b/src/tabris/CryptoKey.ts @@ -24,7 +24,7 @@ export type AlgorithmECDSA = { namedCurve: 'P-256' }; -export type GenerateKeyOptions = { inTee?: boolean, usageRequiresAuth?: boolean }; +export type GenerateKeyOptions = { usageRequiresAuth?: boolean }; export default class CryptoKey { @@ -123,7 +123,6 @@ export class _CryptoKey extends NativeObject { algorithm: AlgorithmECDH | AlgorithmECDSA, extractable: boolean, keyUsages: string[], - inTee?: boolean, usageRequiresAuth?: boolean ): Promise { return new Promise((onSuccess, onError) => @@ -131,7 +130,6 @@ export class _CryptoKey extends NativeObject { algorithm, extractable, keyUsages, - inTee, usageRequiresAuth, onSuccess, onError: wrapErrorCb(onError) diff --git a/test/tabris/Crypto.test.ts b/test/tabris/Crypto.test.ts index 853dc83d..5fce4d0d 100644 --- a/test/tabris/Crypto.test.ts +++ b/test/tabris/Crypto.test.ts @@ -661,7 +661,7 @@ describe('Crypto', function() { it('checks format values', async function() { params[0] = 'foo'; await expect(importKey()) - .rejectedWith(TypeError, 'format must be "spki", "pkcs8", "raw" or "teeKeyHandle", got "foo"'); + .rejectedWith(TypeError, 'format must be "spki", "pkcs8" or "raw", got "foo"'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); @@ -778,7 +778,7 @@ describe('Crypto', function() { // @ts-ignore params[0] = 'foo'; await expect(exportKey()) - .rejectedWith(TypeError, 'format must be "raw", "spki" or "teeKeyHandle", got "foo"'); + .rejectedWith(TypeError, 'format must be "raw" or "spki", got "foo"'); expect(client.calls({op: 'call', method: 'subtleExportKey'}).length).to.equal(0); }); @@ -1065,7 +1065,7 @@ describe('Crypto', function() { {name: 'ECDSA', namedCurve: 'P-256'}, true, ['foo', 'bar'], - {inTee: true, usageRequiresAuth: true} + {usageRequiresAuth: false} ]; }); @@ -1078,8 +1078,7 @@ describe('Crypto', function() { algorithm: {name: 'ECDSA', namedCurve: 'P-256'}, extractable: true, keyUsages: ['foo', 'bar'], - inTee: true, - usageRequiresAuth: true + usageRequiresAuth: false }); }); @@ -1138,35 +1137,28 @@ describe('Crypto', function() { expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); - it('checks options.inTee type', async function() { - params[3] = {inTee: null, usageRequiresAuth: true}; - await expect(generateKey()) - .rejectedWith(TypeError, 'Expected options.inTee to be a boolean, got null'); - expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); - }); - it('checks options.usageRequiresAuth type', async function() { - params[3] = {inTee: true, usageRequiresAuth: null}; + params[3] = {usageRequiresAuth: null}; await expect(generateKey()) .rejectedWith(TypeError, 'Expected options.usageRequiresAuth to be a boolean, got null'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); - it('rejects options.usageRequiresAuth when options.inTee is not set and platform is not Android', async function() { - (tabris as any).device.platform = 'iOS'; + it('rejects options.usageRequiresAuth when key is extractable', async function() { + params[1] = true; params[3] = {usageRequiresAuth: true}; await expect(generateKey()) - .rejectedWith(TypeError, 'options.usageRequiresAuth is only supported for keys not in TEE on Android'); + .rejectedWith(TypeError, 'options.usageRequiresAuth is only supported for non-extractable EC keys'); expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.equal(0); }); - it('does not reject options.usageRequiresAuth when options.inTee is not set and platform is Android', - async function() { - (tabris as any).device.platform = 'Android'; - params[3] = {usageRequiresAuth: true}; - await generateKey(param => param.onSuccess()); - expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.be.greaterThan(0); - }); + it('does not reject options.usageRequiresAuth for non-extractable EC keys', async function() { + (tabris as any).device.platform = 'Android'; + params[1] = false; + params[3] = {usageRequiresAuth: true}; + await generateKey(param => param.onSuccess()); + expect(client.calls({op: 'create', type: 'tabris.CryptoKey'}).length).to.be.greaterThan(0); + }); });