├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── .yarnrc ├── LICENSE ├── Makefile ├── babel.config.js ├── bin └── install.sh ├── bootstrap-roles.sql ├── docker-compose.yml ├── extensions └── @launchql │ └── base32 │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── LICENSE │ ├── Makefile │ ├── deploy │ └── schemas │ │ └── base32 │ │ ├── procedures │ │ ├── decode.sql │ │ └── encode.sql │ │ └── schema.sql │ ├── launchql-base32.control │ ├── package.json │ ├── readme.md │ ├── revert │ └── schemas │ │ └── base32 │ │ ├── procedures │ │ ├── decode.sql │ │ └── encode.sql │ │ └── schema.sql │ ├── sqitch.conf │ ├── sqitch.plan │ ├── sql │ └── launchql-base32--0.0.3.sql │ ├── test │ ├── __snapshots__ │ │ ├── base32.decode.test.js.snap │ │ └── base32.encode.test.js.snap │ ├── base32.decode.test.js │ ├── base32.encode.test.js │ └── utils │ │ └── index.js │ └── verify │ └── schemas │ └── base32 │ ├── procedures │ ├── decode.sql │ └── encode.sql │ └── schema.sql ├── jest.config.js ├── lerna.json ├── package.json ├── packages └── totp │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .npmignore │ ├── .npmrc │ ├── LICENSE │ ├── Makefile │ ├── deploy │ └── schemas │ │ └── totp │ │ ├── procedures │ │ ├── generate_totp.sql │ │ ├── random_base32.sql │ │ └── urlencode.sql │ │ └── schema.sql │ ├── launchql-totp.control │ ├── package.json │ ├── readme.md │ ├── revert │ └── schemas │ │ └── totp │ │ ├── procedures │ │ ├── generate_totp.sql │ │ ├── random_base32.sql │ │ └── urlencode.sql │ │ └── schema.sql │ ├── sqitch.conf │ ├── sqitch.plan │ ├── sql │ └── launchql-totp--0.0.3.sql │ ├── test │ ├── __snapshots__ │ │ └── algo.totp.test.js.snap │ ├── algo.totp.test.js │ ├── totp.test.js │ └── utils │ │ └── index.js │ └── verify │ └── schemas │ └── totp │ ├── procedures │ ├── generate_totp.sql │ ├── random_base32.sql │ └── urlencode.sql │ └── schema.sql ├── readme.md ├── skitch.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier'], 3 | extends: ['eslint:recommended', 'prettier'], 4 | parser: 'babel-eslint', 5 | globals: { 6 | jasmine: true 7 | }, 8 | parserOptions: { 9 | ecmaVersion: 11, 10 | sourceType: 'module', 11 | ecmaFeatures: { 12 | jsx: true 13 | } 14 | }, 15 | env: { 16 | es6: true, 17 | browser: true, 18 | node: true, 19 | jest: true 20 | }, 21 | rules: { 22 | 'no-debugger': 2, 23 | 'no-alert': 2, 24 | 'no-await-in-loop': 0, 25 | 'no-prototype-builtins': 0, 26 | 'no-return-assign': ['error', 'except-parens'], 27 | 'no-restricted-syntax': [ 28 | 2, 29 | 'ForInStatement', 30 | 'LabeledStatement', 31 | 'WithStatement' 32 | ], 33 | 'no-unused-vars': [ 34 | 0, 35 | { 36 | ignoreSiblings: true, 37 | argsIgnorePattern: 'React|res|next|^_' 38 | } 39 | ], 40 | 'prefer-const': [ 41 | 'error', 42 | { 43 | destructuring: 'all' 44 | } 45 | ], 46 | 'no-unused-expressions': [ 47 | 2, 48 | { 49 | allowTaggedTemplates: true 50 | } 51 | ], 52 | 'no-console': 1, 53 | 'comma-dangle': 2, 54 | 'jsx-quotes': [2, 'prefer-double'], 55 | 'linebreak-style': ['error', 'unix'], 56 | quotes: [ 57 | 2, 58 | 'single', 59 | { 60 | avoidEscape: true, 61 | allowTemplateLiterals: true 62 | } 63 | ], 64 | 'prettier/prettier': [ 65 | 'error', 66 | { 67 | trailingComma: 'none', 68 | singleQuote: true, 69 | printWidth: 80 70 | } 71 | ] 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .eslintcache 4 | *.log 5 | **/node_modules 6 | coverage 7 | packages/**/build 8 | packages/**/main 9 | packages/**/module -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .eslintcache 4 | *.log 5 | **/node_modules 6 | coverage 7 | packages/**/build 8 | packages/**/main 9 | packages/**/module -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | notifications: 5 | email: false 6 | services: 7 | - docker 8 | env: 9 | global: 10 | - POSTGRES_USER=postgres 11 | - POSTGRES_PASSWORD=password 12 | - PGPORT=7777 13 | - PGTEMPLATE_DATABASE=testing-template-db 14 | - PGHOST=localhost 15 | - PGPASSWORD=password 16 | - PGUSER=postgres 17 | - APP_USER=app_user 18 | - APP_PASSWORD=app_password 19 | matrix: 20 | - PACKAGE=packages/totp 21 | 22 | before_install: 23 | - yarn install 24 | - docker run -p 7777:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_USER=postgres --name postgres -v `pwd`/bin:/sql-bin -v `pwd`/extensions:/sql-extensions -v `pwd`/packages:/sql-packages -d pyramation/postgis 25 | - sleep 3 26 | - while ! docker exec -it postgres pg_isready -U postgres -h 127.0.0.1; do echo "$(date) - waiting for database to start"; sleep 1; done 27 | - while ! docker exec postgres /sql-bin/install.sh; do echo "installing plugins"; sleep 3; done 28 | - psql -f ./bootstrap-roles.sql postgres 29 | script: 30 | - cd $PACKAGE 31 | - yarn install 32 | - yarn test 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "[javascriptreact]": { 4 | "editor.formatOnSave": false 5 | }, 6 | "[javascript]": { 7 | "editor.formatOnSave": false 8 | }, 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true 11 | }, 12 | "eslint.validate": [ 13 | "javascript", 14 | "javascriptreact" 15 | ] 16 | } -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org/" 2 | workspaces-experimental true 3 | save-prefix false 4 | save-exact true 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Web, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | up: 3 | docker-compose up -d 4 | 5 | down: 6 | docker-compose down -v 7 | 8 | ssh: 9 | docker exec -it totp-postgres /bin/bash 10 | 11 | install: 12 | docker exec totp-postgres /sql-bin/install.sh 13 | 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrcRoots: 'packages/*', 3 | presets: ['@pyramation/env'] 4 | }; 5 | -------------------------------------------------------------------------------- /bin/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function installit { 4 | DIR=$(pwd) 5 | 6 | if [ -d "$1" ] 7 | then 8 | echo "SQL Module Directory $1 exists." 9 | echo $1 10 | cd $1 11 | 12 | for x in $(find ./ -type f -name "sqitch.plan") 13 | do 14 | orig=$(pwd) 15 | dir=$(dirname $x) 16 | cd $dir 17 | make install 18 | cd $orig 19 | done 20 | cd $DIR 21 | else 22 | echo "Error: SQL MODULE Directory $1 does not exist, don't worry, moving on." 23 | fi 24 | 25 | } 26 | 27 | installit /sql-extensions 28 | installit /sql-packages -------------------------------------------------------------------------------- /bootstrap-roles.sql: -------------------------------------------------------------------------------- 1 | -- anonymous 2 | CREATE ROLE anonymous; 3 | 4 | ALTER USER anonymous WITH NOCREATEDB; 5 | 6 | ALTER USER anonymous WITH NOSUPERUSER; 7 | 8 | ALTER USER anonymous WITH NOCREATEROLE; 9 | 10 | ALTER USER anonymous WITH NOLOGIN; 11 | 12 | ALTER USER anonymous WITH NOREPLICATION; 13 | 14 | ALTER USER anonymous WITH NOBYPASSRLS; 15 | 16 | -- authenticated 17 | CREATE ROLE authenticated; 18 | 19 | ALTER USER authenticated WITH NOCREATEDB; 20 | 21 | ALTER USER authenticated WITH NOSUPERUSER; 22 | 23 | ALTER USER authenticated WITH NOCREATEROLE; 24 | 25 | ALTER USER authenticated WITH NOLOGIN; 26 | 27 | ALTER USER authenticated WITH NOREPLICATION; 28 | 29 | ALTER USER authenticated WITH NOBYPASSRLS; 30 | 31 | -- administrator 32 | CREATE ROLE administrator; 33 | 34 | ALTER USER administrator WITH NOCREATEDB; 35 | 36 | ALTER USER administrator WITH NOSUPERUSER; 37 | 38 | ALTER USER administrator WITH NOCREATEROLE; 39 | 40 | ALTER USER administrator WITH NOLOGIN; 41 | 42 | ALTER USER administrator WITH NOREPLICATION; 43 | 44 | -- they CAN bypass RLS 45 | ALTER USER administrator WITH BYPASSRLS; 46 | 47 | -- app user 48 | CREATE ROLE app_user LOGIN PASSWORD 'app_password'; 49 | 50 | GRANT anonymous TO app_user; 51 | 52 | GRANT authenticated TO app_user; 53 | 54 | -- admin user 55 | CREATE ROLE app_admin LOGIN PASSWORD 'admin_password'; 56 | 57 | GRANT anonymous TO administrator; 58 | 59 | GRANT authenticated TO administrator; 60 | 61 | GRANT administrator TO app_admin; 62 | 63 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | postgres: 4 | container_name: totp-postgres 5 | image: pyramation/postgis 6 | environment: 7 | - "POSTGRES_USER=postgres" 8 | - "POSTGRES_PASSWORD=password" 9 | ports: 10 | - "5432:5432" 11 | expose: 12 | - "5432" 13 | volumes: 14 | - ./bin:/sql-bin 15 | - ./packages:/sql-packages 16 | - ./extensions:/sql-extensions 17 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-class-properties", 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-proposal-export-default-from", 6 | "@babel/plugin-transform-runtime", 7 | "import-graphql" 8 | ], 9 | "presets": [ 10 | "@babel/env" 11 | ] 12 | } -------------------------------------------------------------------------------- /extensions/@launchql/base32/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | main/ 4 | module/ 5 | coverage/ -------------------------------------------------------------------------------- /extensions/@launchql/base32/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier'], 3 | extends: ['eslint:recommended', 'prettier'], 4 | parser: 'babel-eslint', 5 | globals: { 6 | jasmine: true 7 | }, 8 | parserOptions: { 9 | ecmaVersion: 11, 10 | sourceType: 'module', 11 | ecmaFeatures: { 12 | jsx: true 13 | } 14 | }, 15 | env: { 16 | es6: true, 17 | browser: true, 18 | node: true, 19 | jest: true 20 | }, 21 | rules: { 22 | 'no-debugger': 2, 23 | 'no-alert': 2, 24 | 'no-await-in-loop': 0, 25 | 'no-prototype-builtins': 0, 26 | 'no-return-assign': ['error', 'except-parens'], 27 | 'no-restricted-syntax': [ 28 | 2, 29 | 'ForInStatement', 30 | 'LabeledStatement', 31 | 'WithStatement' 32 | ], 33 | 'no-unused-vars': [ 34 | 0, 35 | { 36 | ignoreSiblings: true, 37 | argsIgnorePattern: 'React|res|next|^_' 38 | } 39 | ], 40 | 'prefer-const': [ 41 | 'error', 42 | { 43 | destructuring: 'all' 44 | } 45 | ], 46 | 'no-unused-expressions': [ 47 | 2, 48 | { 49 | allowTaggedTemplates: true 50 | } 51 | ], 52 | 'no-console': 1, 53 | 'comma-dangle': 2, 54 | 'jsx-quotes': [2, 'prefer-double'], 55 | 'linebreak-style': ['error', 'unix'], 56 | quotes: [ 57 | 2, 58 | 'single', 59 | { 60 | avoidEscape: true, 61 | allowTemplateLiterals: true 62 | } 63 | ], 64 | 'prettier/prettier': [ 65 | 'error', 66 | { 67 | trailingComma: 'none', 68 | singleQuote: true, 69 | printWidth: 80 70 | } 71 | ] 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Web, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION = launchql-base32 2 | DATA = sql/launchql-base32--0.0.3.sql 3 | 4 | PG_CONFIG = pg_config 5 | PGXS := $(shell $(PG_CONFIG) --pgxs) 6 | include $(PGXS) 7 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/deploy/schemas/base32/procedures/decode.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/base32/procedures/decode to pg 2 | 3 | -- requires: schemas/base32/schema 4 | -- requires: schemas/base32/procedures/encode 5 | 6 | BEGIN; 7 | 8 | -- 'I' => '8' 9 | CREATE FUNCTION base32.base32_alphabet_to_decimal( 10 | input text 11 | ) returns text as $$ 12 | DECLARE 13 | alphabet text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 14 | alpha int; 15 | BEGIN 16 | alpha = position(input in alphabet) - 1; 17 | IF (alpha < 0) THEN 18 | RETURN '='; 19 | END IF; 20 | RETURN alpha::text; 21 | END; 22 | $$ 23 | LANGUAGE 'plpgsql' IMMUTABLE; 24 | 25 | -- INQXI=== => ['8', '13', '16', '23', '8', '=', '=', '='] 26 | CREATE FUNCTION base32.base32_to_decimal( 27 | input text 28 | ) returns text[] as $$ 29 | DECLARE 30 | i int; 31 | output text[]; 32 | BEGIN 33 | input = upper(input); 34 | FOR i IN 1 .. character_length(input) LOOP 35 | output = array_append(output, base32.base32_alphabet_to_decimal(substring(input from i for 1))); 36 | END LOOP; 37 | RETURN output; 38 | END; 39 | $$ 40 | LANGUAGE 'plpgsql' STABLE; 41 | 42 | -- ['8', '13', '16', '23', '8', '=', '=', '='] 43 | -- [ '01000', '01101', '10000', '10111', '01000', 'xxxxx', 'xxxxx', 'xxxxx' ] 44 | CREATE FUNCTION base32.decimal_to_chunks( 45 | input text[] 46 | ) returns text[] as $$ 47 | DECLARE 48 | i int; 49 | part text; 50 | output text[]; 51 | BEGIN 52 | FOR i IN 1 .. cardinality(input) LOOP 53 | part = input[i]; 54 | IF (part = '=') THEN 55 | output = array_append(output, 'xxxxx'); 56 | ELSE 57 | output = array_append(output, right(base32.to_binary(part::int), 5)); 58 | END IF; 59 | END LOOP; 60 | RETURN output; 61 | END; 62 | $$ 63 | LANGUAGE 'plpgsql' STABLE; 64 | 65 | CREATE FUNCTION base32.base32_alphabet_to_decimal_int( 66 | input text 67 | ) returns int as $$ 68 | DECLARE 69 | alphabet text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 70 | alpha int; 71 | BEGIN 72 | alpha = position(input in alphabet) - 1; 73 | RETURN alpha; 74 | END; 75 | $$ 76 | LANGUAGE 'plpgsql' IMMUTABLE; 77 | 78 | -- this emulates the >>> (unsigned right shift) 79 | -- https://stackoverflow.com/questions/41134337/unsigned-right-shift-zero-fill-right-shift-in-php-java-javascript-equiv 80 | CREATE FUNCTION base32.zero_fill( 81 | a int, b int 82 | ) returns bigint as $$ 83 | DECLARE 84 | bin text; 85 | m int; 86 | BEGIN 87 | 88 | IF (b >= 32 OR b < -32) THEN 89 | m = b/32; 90 | b = b-(m*32); 91 | END IF; 92 | 93 | IF (b < 0) THEN 94 | b = 32 + b; 95 | END IF; 96 | 97 | IF (b = 0) THEN 98 | return ((a>>1)&2147483647)*2::bigint+((a>>b)&1); 99 | END IF; 100 | 101 | IF (a < 0) THEN 102 | a = (a >> 1); 103 | a = a & 2147483647; -- 0x7fffffff 104 | a = a | 1073741824; -- 0x40000000 105 | a = (a >> (b - 1)); 106 | ELSE 107 | a = (a >> b); 108 | END IF; 109 | 110 | RETURN a; 111 | END; 112 | $$ 113 | LANGUAGE 'plpgsql' IMMUTABLE; 114 | 115 | CREATE FUNCTION base32.valid( 116 | input text 117 | ) returns boolean as $$ 118 | BEGIN 119 | IF (upper(input) ~* '^[A-Z2-7]+=*$') THEN 120 | RETURN true; 121 | END IF; 122 | RETURN false; 123 | END; 124 | $$ 125 | LANGUAGE 'plpgsql' IMMUTABLE; 126 | 127 | CREATE FUNCTION base32.decode( 128 | input text 129 | ) returns text as $$ 130 | DECLARE 131 | i int; 132 | arr int[]; 133 | output text[]; 134 | len int; 135 | num int; 136 | 137 | value int = 0; 138 | index int = 0; 139 | bits int = 0; 140 | BEGIN 141 | len = character_length(input); 142 | IF (len = 0) THEN 143 | RETURN ''; 144 | END IF; 145 | 146 | IF (NOT base32.valid(input)) THEN 147 | RAISE EXCEPTION 'INVALID_BASE32'; 148 | END IF; 149 | 150 | input = replace(input, '=', ''); 151 | input = upper(input); 152 | len = character_length(input); 153 | num = len * 5 / 8; 154 | 155 | select array(select * from generate_series(1,num)) 156 | INTO arr; 157 | 158 | FOR i IN 1 .. len LOOP 159 | value = (value << 5) | base32.base32_alphabet_to_decimal_int(substring(input from i for 1)); 160 | bits = bits + 5; 161 | IF (bits >= 8) THEN 162 | arr[index] = base32.zero_fill(value, (bits - 8)) & 255; -- arr[index] = (value >>> (bits - 8)) & 255; 163 | index = index + 1; 164 | bits = bits - 8; 165 | END IF; 166 | END LOOP; 167 | 168 | len = cardinality(arr); 169 | FOR i IN 0 .. len-2 LOOP 170 | output = array_append(output, chr(arr[i])); 171 | END LOOP; 172 | 173 | RETURN array_to_string(output, ''); 174 | END; 175 | $$ 176 | LANGUAGE 'plpgsql' STABLE; 177 | 178 | COMMIT; -------------------------------------------------------------------------------- /extensions/@launchql/base32/deploy/schemas/base32/procedures/encode.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/base32/procedures/encode to pg 2 | 3 | -- requires: schemas/base32/schema 4 | 5 | -- https://tools.ietf.org/html/rfc4648 6 | -- https://www.youtube.com/watch?v=Va8FLD-iuTg 7 | 8 | BEGIN; 9 | 10 | -- '01000011' => 67 11 | CREATE FUNCTION base32.binary_to_int( 12 | input text 13 | ) returns int as $$ 14 | DECLARE 15 | i int; 16 | buf text; 17 | BEGIN 18 | buf = 'SELECT B''' || input || '''::int'; 19 | EXECUTE buf INTO i; 20 | RETURN i; 21 | END; 22 | $$ 23 | LANGUAGE 'plpgsql' IMMUTABLE; 24 | 25 | -- ASCII decimal values Cat => [67,97,116] 26 | CREATE FUNCTION base32.to_ascii( 27 | input text 28 | ) returns int[] as $$ 29 | DECLARE 30 | i int; 31 | output int[]; 32 | BEGIN 33 | FOR i IN 1 .. character_length(input) LOOP 34 | output = array_append(output, ascii(substring(input from i for 1))); 35 | END LOOP; 36 | RETURN output; 37 | END; 38 | $$ 39 | LANGUAGE 'plpgsql' IMMUTABLE; 40 | 41 | -- 67 => '01000011' 42 | CREATE FUNCTION base32.to_binary( 43 | input int 44 | ) returns text as $$ 45 | DECLARE 46 | i int = 1; 47 | j int = 0; 48 | output char[] = ARRAY['x', 'x', 'x', 'x', 'x', 'x', 'x', 'x']; 49 | BEGIN 50 | WHILE i < 256 LOOP 51 | output[8-j] = (CASE WHEN (input & i) > 0 THEN '1' ELSE '0' END)::char; 52 | i = i << 1; 53 | j = j + 1; 54 | END LOOP; 55 | RETURN array_to_string(output, ''); 56 | END; 57 | $$ 58 | LANGUAGE 'plpgsql' IMMUTABLE; 59 | 60 | -- [67,97,116] => [01000011, 01100001, 01110100] 61 | CREATE FUNCTION base32.to_binary( 62 | input int[] 63 | ) returns text[] as $$ 64 | DECLARE 65 | i int; 66 | output text[]; 67 | BEGIN 68 | FOR i IN 1 .. cardinality(input) LOOP 69 | output = array_append(output, base32.to_binary(input[i])); 70 | END LOOP; 71 | RETURN output; 72 | END; 73 | $$ 74 | LANGUAGE 'plpgsql' IMMUTABLE; 75 | 76 | -- convert an input byte stream into group of 5 bytes 77 | -- if there are less than 5, adding padding 78 | 79 | -- [01000011, 01100001, 01110100, xxxxxxxx, xxxxxxxx] 80 | 81 | CREATE FUNCTION base32.to_groups( 82 | input text[] 83 | ) returns text[] as $$ 84 | DECLARE 85 | i int; 86 | output text[]; 87 | len int = cardinality(input); 88 | BEGIN 89 | IF ( len % 5 = 0 ) THEN 90 | RETURN input; 91 | END IF; 92 | FOR i IN 1 .. 5 - (len % 5) LOOP 93 | input = array_append(input, 'xxxxxxxx'); 94 | END LOOP; 95 | RETURN input; 96 | END; 97 | $$ 98 | LANGUAGE 'plpgsql' IMMUTABLE; 99 | 100 | -- break these into 5 bit chunks (5 * 8 = 40 bits, when we 40/5 = 8 new elements of 5 bits each) 101 | 102 | -- [01000, 01101, 10000, 10111, 0100x, xxxxx, xxxxx, xxxxx] 103 | 104 | CREATE FUNCTION base32.string_nchars(text, integer) 105 | RETURNS text[] AS $$ 106 | SELECT ARRAY(SELECT substring($1 from n for $2) 107 | FROM generate_series(1, length($1), $2) n); 108 | $$ LANGUAGE sql IMMUTABLE; 109 | 110 | CREATE FUNCTION base32.to_chunks( 111 | input text[] 112 | ) returns text[] as $$ 113 | DECLARE 114 | i int; 115 | output text[]; 116 | str text; 117 | len int = cardinality(input); 118 | BEGIN 119 | RETURN base32.string_nchars(array_to_string(input, ''), 5); 120 | END; 121 | $$ 122 | LANGUAGE 'plpgsql' IMMUTABLE; 123 | 124 | -- if a chunk has a mix of real bits (0|1) and empty (x), replace x with 0 125 | 126 | -- [01000, 01101, 10000, 10111, 0100x, xxxxx, xxxxx, xxxxx] 127 | -- [01000, 01101, 10000, 10111, 01000, xxxxx, xxxxx, xxxxx] 128 | 129 | CREATE FUNCTION base32.fill_chunks( 130 | input text[] 131 | ) returns text[] as $$ 132 | DECLARE 133 | i int; 134 | output text[]; 135 | chunk text; 136 | len int = cardinality(input); 137 | BEGIN 138 | FOR i IN 1 .. len LOOP 139 | chunk = input[i]; 140 | IF (chunk ~* '[0-1]+') THEN 141 | chunk = replace(chunk, 'x', '0'); 142 | END IF; 143 | output = array_append(output, chunk); 144 | END LOOP; 145 | RETURN output; 146 | END; 147 | $$ 148 | LANGUAGE 'plpgsql' IMMUTABLE; 149 | 150 | -- convert to decimal value 151 | 152 | -- [01000, 01101, 10000, 10111, 01000, xxxxx, xxxxx, xxxxx] 153 | -- [0b01000, 0b01101, 0b10000, 0b10111, 0b01000, xxxxx, xxxxx, xxxxx] 154 | -- [ 8, 13, 16, 23, 8, '=', '=', '=' ] 155 | 156 | CREATE FUNCTION base32.to_decimal( 157 | input text[] 158 | ) returns text[] as $$ 159 | DECLARE 160 | i int; 161 | output text[]; 162 | chunk text; 163 | buf text; 164 | len int = cardinality(input); 165 | BEGIN 166 | FOR i IN 1 .. len LOOP 167 | chunk = input[i]; 168 | IF (chunk ~* '[x]+') THEN 169 | chunk = '='; 170 | ELSE 171 | chunk = base32.binary_to_int(input[i])::text; 172 | END IF; 173 | output = array_append(output, chunk); 174 | END LOOP; 175 | RETURN output; 176 | END; 177 | $$ 178 | LANGUAGE 'plpgsql' IMMUTABLE; 179 | 180 | 181 | -- Table 3: The Base 32 Alphabet 182 | 183 | -- 0 A 9 J 18 S 27 3 184 | -- 1 B 10 K 19 T 28 4 185 | -- 2 C 11 L 20 U 29 5 186 | -- 3 D 12 M 21 V 30 6 187 | -- 4 E 13 N 22 W 31 7 188 | -- 5 F 14 O 23 X 189 | -- 6 G 15 P 24 Y (pad) = 190 | -- 7 H 16 Q 25 Z 191 | -- 8 I 17 R 26 2 192 | 193 | CREATE FUNCTION base32.base32_alphabet( 194 | input int 195 | ) returns char as $$ 196 | DECLARE 197 | alphabet text[] = ARRAY[ 198 | 'A', 'B', 'C', 'D', 'E', 'F', 199 | 'G', 'H', 'I', 'J', 'K', 'L', 200 | 'M', 'N', 'O', 'P', 'Q', 'R', 201 | 'S', 'T', 'U', 'V', 'W', 'X', 202 | 'Y', 'Z', '2', '3', '4', '5', 203 | '6', '7' 204 | ]::text; 205 | BEGIN 206 | RETURN alphabet[input+1]; 207 | END; 208 | $$ 209 | LANGUAGE 'plpgsql' IMMUTABLE; 210 | 211 | -- [ 8, 13, 16, 23, 8, '=', '=', '=' ] 212 | -- [ I, N, Q, X, I, '=', '=', '=' ] 213 | 214 | CREATE FUNCTION base32.to_base32( 215 | input text[] 216 | ) returns text as $$ 217 | DECLARE 218 | i int; 219 | output text[]; 220 | chunk text; 221 | buf text; 222 | len int = cardinality(input); 223 | BEGIN 224 | FOR i IN 1 .. len LOOP 225 | chunk = input[i]; 226 | IF (chunk = '=') THEN 227 | chunk = '='; 228 | ELSE 229 | chunk = base32.base32_alphabet(chunk::int); 230 | END IF; 231 | output = array_append(output, chunk); 232 | END LOOP; 233 | RETURN array_to_string(output, ''); 234 | END; 235 | $$ 236 | LANGUAGE 'plpgsql' IMMUTABLE; 237 | 238 | CREATE FUNCTION base32.encode( 239 | input text 240 | ) returns text as $$ 241 | BEGIN 242 | IF (character_length(input) = 0) THEN 243 | RETURN ''; 244 | END IF; 245 | 246 | RETURN 247 | base32.to_base32( 248 | base32.to_decimal( 249 | base32.fill_chunks( 250 | base32.to_chunks( 251 | base32.to_groups( 252 | base32.to_binary( 253 | base32.to_ascii( 254 | input 255 | ) 256 | ) 257 | ) 258 | ) 259 | ) 260 | ) 261 | ); 262 | END; 263 | $$ 264 | LANGUAGE 'plpgsql' IMMUTABLE; 265 | 266 | COMMIT; 267 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/deploy/schemas/base32/schema.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/base32/schema to pg 2 | 3 | 4 | BEGIN; 5 | 6 | CREATE SCHEMA base32; 7 | 8 | COMMIT; 9 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/launchql-base32.control: -------------------------------------------------------------------------------- 1 | # launchql-base32 extension 2 | comment = 'launchql-base32 extension' 3 | default_version = '0.0.3' 4 | module_pathname = '$libdir/launchql-base32' 5 | requires = 'pgcrypto,plpgsql' 6 | relocatable = false 7 | superuser = false 8 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "@launchql/base32@0.0.3", 3 | "_id": "@launchql/base32@0.0.3", 4 | "_inBundle": false, 5 | "_integrity": "sha512-q4U1SsmiQCzK4uDmWCe6C8tsPqUjoOZJ9O2AEVr5dhfPWUaSzbjEw1CjHePmaVVkkBQJxmRQk2yVL9QwPrATwg==", 6 | "_location": "/@launchql/base32", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "version", 10 | "registry": true, 11 | "raw": "@launchql/base32@0.0.3", 12 | "name": "@launchql/base32", 13 | "escapedName": "@launchql%2fbase32", 14 | "scope": "@launchql", 15 | "rawSpec": "0.0.3", 16 | "saveSpec": null, 17 | "fetchSpec": "0.0.3" 18 | }, 19 | "_requiredBy": [ 20 | "#USER", 21 | "/" 22 | ], 23 | "_resolved": "https://registry.npmjs.org/@launchql/base32/-/base32-0.0.3.tgz", 24 | "_shasum": "789b03b5dc4eeace8956d7f3af65a7f0c925aa95", 25 | "_spec": "@launchql/base32@0.0.3", 26 | "_where": "/private/var/folders/kn/177lldss4n9fwn0gh5q5wfn00000gn/T/ldpwz8jj4n9ifnai570rb", 27 | "author": { 28 | "name": "Web, Inc.", 29 | "email": "pyramation@gmail.com" 30 | }, 31 | "bundleDependencies": false, 32 | "deprecated": false, 33 | "description": "base32", 34 | "devDependencies": { 35 | "@babel/cli": "7.10.4", 36 | "@babel/core": "7.10.4", 37 | "@babel/plugin-proposal-class-properties": "7.10.4", 38 | "@babel/plugin-proposal-export-default-from": "7.10.4", 39 | "@babel/plugin-proposal-object-rest-spread": "7.10.4", 40 | "@babel/plugin-transform-runtime": "7.10.4", 41 | "@babel/preset-env": "7.10.4", 42 | "@babel/runtime": "^7.4.2", 43 | "@launchql/db-testing": "latest", 44 | "@launchql/graphql-testing": "latest", 45 | "babel-eslint": "10.1.0", 46 | "babel-jest": "26.1.0", 47 | "babel-plugin-import-graphql": "2.7.0", 48 | "babel-plugin-macros": "2.8.0", 49 | "eslint": "^7.3.1", 50 | "eslint-config-prettier": "^6.10.0", 51 | "eslint-plugin-prettier": "^3.1.2", 52 | "graphql": "^14.0.2", 53 | "graphql-tag": "2.10.3", 54 | "jest": "26.1.0", 55 | "jest-in-case": "1.0.2", 56 | "prettier": "2.0.5", 57 | "regenerator-runtime": "^0.13.2" 58 | }, 59 | "gitHead": "837c793730ea982dd0c2cd9c16e92011ced2c4e6", 60 | "name": "@launchql/base32", 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "scripts": { 65 | "prepare": "lql package", 66 | "test": "FAST_TEST=1 launchql-templatedb && jest", 67 | "test:watch": "FAST_TEST=1 jest --watch" 68 | }, 69 | "version": "0.0.3" 70 | } 71 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/readme.md: -------------------------------------------------------------------------------- 1 | # base32 [![Build Status](https://travis-ci.com/pyramation/base32.svg?branch=master)](https://travis-ci.com/pyramation/base32) 2 | 3 | RFC4648 Base32 encode/decode in plpgsql 4 | 5 | # Usage 6 | 7 | ```sql 8 | select base32.encode('foo'); 9 | -- MZXW6=== 10 | 11 | 12 | select base32.decode('MZXW6==='); 13 | -- foo 14 | ``` 15 | 16 | # credits 17 | 18 | Thanks to 19 | 20 | https://tools.ietf.org/html/rfc4648 21 | 22 | https://www.youtube.com/watch?v=Va8FLD-iuTg 23 | 24 | # Development 25 | 26 | ## start the postgres db process 27 | 28 | First you'll want to start the postgres docker (you can also just use `docker-compose up -d`): 29 | 30 | ```sh 31 | make up 32 | ``` 33 | 34 | ## install modules 35 | 36 | Install modules 37 | 38 | ```sh 39 | yarn install 40 | ``` 41 | 42 | ## install the Postgres extensions 43 | 44 | Now that the postgres process is running, install the extensions: 45 | 46 | ```sh 47 | make install 48 | ``` 49 | 50 | This basically `ssh`s into the postgres instance with the `packages/` folder mounted as a volume, and installs the bundled sql code as pgxn extensions. 51 | 52 | ## testing 53 | 54 | Testing will load all your latest sql changes and create fresh, populated databases for each sqitch module in `packages/`. 55 | 56 | ```sh 57 | yarn test:watch 58 | ``` 59 | 60 | ## building new modules 61 | 62 | Create a new folder in `packages/` 63 | 64 | ```sh 65 | lql init 66 | ``` 67 | 68 | Then, run a generator: 69 | 70 | ```sh 71 | lql generate 72 | ``` 73 | 74 | You can also add arguments if you already know what you want to do: 75 | 76 | ```sh 77 | lql generate schema --schema myschema 78 | lql generate table --schema myschema --table mytable 79 | ``` 80 | 81 | ## deploy code as extensions 82 | 83 | `cd` into `packages/`, and run `lql package`. This will make an sql file in `packages//sql/` used for `CREATE EXTENSION` calls to install your sqitch module as an extension. 84 | 85 | ## recursive deploy 86 | 87 | You can also deploy all modules utilizing versioning as sqtich modules. Remove `--createdb` if you already created your db: 88 | 89 | ```sh 90 | lql deploy awesome-db --yes --recursive --createdb 91 | ``` 92 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/revert/schemas/base32/procedures/decode.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/base32/procedures/decode from pg 2 | 3 | BEGIN; 4 | 5 | DROP FUNCTION base32.decode; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/revert/schemas/base32/procedures/encode.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/base32/procedures/encode from pg 2 | 3 | BEGIN; 4 | 5 | DROP FUNCTION base32.encode; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/revert/schemas/base32/schema.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/base32/schema from pg 2 | 3 | BEGIN; 4 | 5 | DROP SCHEMA base32; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/sqitch.conf: -------------------------------------------------------------------------------- 1 | [core] 2 | engine = pg 3 | # plan_file = sqitch.plan 4 | # top_dir = . 5 | # [engine "pg"] 6 | # target = db:pg: 7 | # registry = sqitch 8 | # client = psql 9 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/sqitch.plan: -------------------------------------------------------------------------------- 1 | %syntax-version=1.0.0 2 | %project=launchql-base32 3 | %uri=launchql-base32 4 | 5 | schemas/base32/schema 2017-08-11T08:11:51Z skitch # add schemas/base32/schema 6 | schemas/base32/procedures/encode [schemas/base32/schema] 2017-08-11T08:11:51Z skitch # add schemas/base32/procedures/encode 7 | schemas/base32/procedures/decode [schemas/base32/schema schemas/base32/procedures/encode] 2017-08-11T08:11:51Z skitch # add schemas/base32/procedures/decode -------------------------------------------------------------------------------- /extensions/@launchql/base32/sql/launchql-base32--0.0.3.sql: -------------------------------------------------------------------------------- 1 | \echo Use "CREATE EXTENSION launchql-base32" to load this file. \quit 2 | CREATE SCHEMA base32; 3 | 4 | CREATE FUNCTION base32.binary_to_int ( input text ) RETURNS int AS $EOFCODE$ 5 | DECLARE 6 | i int; 7 | buf text; 8 | BEGIN 9 | buf = 'SELECT B''' || input || '''::int'; 10 | EXECUTE buf INTO i; 11 | RETURN i; 12 | END; 13 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 14 | 15 | CREATE FUNCTION base32.to_ascii ( input text ) RETURNS int[] AS $EOFCODE$ 16 | DECLARE 17 | i int; 18 | output int[]; 19 | BEGIN 20 | FOR i IN 1 .. character_length(input) LOOP 21 | output = array_append(output, ascii(substring(input from i for 1))); 22 | END LOOP; 23 | RETURN output; 24 | END; 25 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 26 | 27 | CREATE FUNCTION base32.to_binary ( input int ) RETURNS text AS $EOFCODE$ 28 | DECLARE 29 | i int = 1; 30 | j int = 0; 31 | output char[] = ARRAY['x', 'x', 'x', 'x', 'x', 'x', 'x', 'x']; 32 | BEGIN 33 | WHILE i < 256 LOOP 34 | output[8-j] = (CASE WHEN (input & i) > 0 THEN '1' ELSE '0' END)::char; 35 | i = i << 1; 36 | j = j + 1; 37 | END LOOP; 38 | RETURN array_to_string(output, ''); 39 | END; 40 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 41 | 42 | CREATE FUNCTION base32.to_binary ( input int[] ) RETURNS text[] AS $EOFCODE$ 43 | DECLARE 44 | i int; 45 | output text[]; 46 | BEGIN 47 | FOR i IN 1 .. cardinality(input) LOOP 48 | output = array_append(output, base32.to_binary(input[i])); 49 | END LOOP; 50 | RETURN output; 51 | END; 52 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 53 | 54 | CREATE FUNCTION base32.to_groups ( input text[] ) RETURNS text[] AS $EOFCODE$ 55 | DECLARE 56 | i int; 57 | output text[]; 58 | len int = cardinality(input); 59 | BEGIN 60 | IF ( len % 5 = 0 ) THEN 61 | RETURN input; 62 | END IF; 63 | FOR i IN 1 .. 5 - (len % 5) LOOP 64 | input = array_append(input, 'xxxxxxxx'); 65 | END LOOP; 66 | RETURN input; 67 | END; 68 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 69 | 70 | CREATE FUNCTION base32.string_nchars ( text, int ) RETURNS text[] AS $EOFCODE$ 71 | SELECT ARRAY(SELECT substring($1 from n for $2) 72 | FROM generate_series(1, length($1), $2) n); 73 | $EOFCODE$ LANGUAGE sql IMMUTABLE; 74 | 75 | CREATE FUNCTION base32.to_chunks ( input text[] ) RETURNS text[] AS $EOFCODE$ 76 | DECLARE 77 | i int; 78 | output text[]; 79 | str text; 80 | len int = cardinality(input); 81 | BEGIN 82 | RETURN base32.string_nchars(array_to_string(input, ''), 5); 83 | END; 84 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 85 | 86 | CREATE FUNCTION base32.fill_chunks ( input text[] ) RETURNS text[] AS $EOFCODE$ 87 | DECLARE 88 | i int; 89 | output text[]; 90 | chunk text; 91 | len int = cardinality(input); 92 | BEGIN 93 | FOR i IN 1 .. len LOOP 94 | chunk = input[i]; 95 | IF (chunk ~* '[0-1]+') THEN 96 | chunk = replace(chunk, 'x', '0'); 97 | END IF; 98 | output = array_append(output, chunk); 99 | END LOOP; 100 | RETURN output; 101 | END; 102 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 103 | 104 | CREATE FUNCTION base32.to_decimal ( input text[] ) RETURNS text[] AS $EOFCODE$ 105 | DECLARE 106 | i int; 107 | output text[]; 108 | chunk text; 109 | buf text; 110 | len int = cardinality(input); 111 | BEGIN 112 | FOR i IN 1 .. len LOOP 113 | chunk = input[i]; 114 | IF (chunk ~* '[x]+') THEN 115 | chunk = '='; 116 | ELSE 117 | chunk = base32.binary_to_int(input[i])::text; 118 | END IF; 119 | output = array_append(output, chunk); 120 | END LOOP; 121 | RETURN output; 122 | END; 123 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 124 | 125 | CREATE FUNCTION base32.base32_alphabet ( input int ) RETURNS char(1) AS $EOFCODE$ 126 | DECLARE 127 | alphabet text[] = ARRAY[ 128 | 'A', 'B', 'C', 'D', 'E', 'F', 129 | 'G', 'H', 'I', 'J', 'K', 'L', 130 | 'M', 'N', 'O', 'P', 'Q', 'R', 131 | 'S', 'T', 'U', 'V', 'W', 'X', 132 | 'Y', 'Z', '2', '3', '4', '5', 133 | '6', '7' 134 | ]::text; 135 | BEGIN 136 | RETURN alphabet[input+1]; 137 | END; 138 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 139 | 140 | CREATE FUNCTION base32.to_base32 ( input text[] ) RETURNS text AS $EOFCODE$ 141 | DECLARE 142 | i int; 143 | output text[]; 144 | chunk text; 145 | buf text; 146 | len int = cardinality(input); 147 | BEGIN 148 | FOR i IN 1 .. len LOOP 149 | chunk = input[i]; 150 | IF (chunk = '=') THEN 151 | chunk = '='; 152 | ELSE 153 | chunk = base32.base32_alphabet(chunk::int); 154 | END IF; 155 | output = array_append(output, chunk); 156 | END LOOP; 157 | RETURN array_to_string(output, ''); 158 | END; 159 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 160 | 161 | CREATE FUNCTION base32.encode ( input text ) RETURNS text AS $EOFCODE$ 162 | BEGIN 163 | IF (character_length(input) = 0) THEN 164 | RETURN ''; 165 | END IF; 166 | 167 | RETURN 168 | base32.to_base32( 169 | base32.to_decimal( 170 | base32.fill_chunks( 171 | base32.to_chunks( 172 | base32.to_groups( 173 | base32.to_binary( 174 | base32.to_ascii( 175 | input 176 | ) 177 | ) 178 | ) 179 | ) 180 | ) 181 | ) 182 | ); 183 | END; 184 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 185 | 186 | CREATE FUNCTION base32.base32_alphabet_to_decimal ( input text ) RETURNS text AS $EOFCODE$ 187 | DECLARE 188 | alphabet text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 189 | alpha int; 190 | BEGIN 191 | alpha = position(input in alphabet) - 1; 192 | IF (alpha < 0) THEN 193 | RETURN '='; 194 | END IF; 195 | RETURN alpha::text; 196 | END; 197 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 198 | 199 | CREATE FUNCTION base32.base32_to_decimal ( input text ) RETURNS text[] AS $EOFCODE$ 200 | DECLARE 201 | i int; 202 | output text[]; 203 | BEGIN 204 | input = upper(input); 205 | FOR i IN 1 .. character_length(input) LOOP 206 | output = array_append(output, base32.base32_alphabet_to_decimal(substring(input from i for 1))); 207 | END LOOP; 208 | RETURN output; 209 | END; 210 | $EOFCODE$ LANGUAGE plpgsql STABLE; 211 | 212 | CREATE FUNCTION base32.decimal_to_chunks ( input text[] ) RETURNS text[] AS $EOFCODE$ 213 | DECLARE 214 | i int; 215 | part text; 216 | output text[]; 217 | BEGIN 218 | FOR i IN 1 .. cardinality(input) LOOP 219 | part = input[i]; 220 | IF (part = '=') THEN 221 | output = array_append(output, 'xxxxx'); 222 | ELSE 223 | output = array_append(output, right(base32.to_binary(part::int), 5)); 224 | END IF; 225 | END LOOP; 226 | RETURN output; 227 | END; 228 | $EOFCODE$ LANGUAGE plpgsql STABLE; 229 | 230 | CREATE FUNCTION base32.base32_alphabet_to_decimal_int ( input text ) RETURNS int AS $EOFCODE$ 231 | DECLARE 232 | alphabet text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 233 | alpha int; 234 | BEGIN 235 | alpha = position(input in alphabet) - 1; 236 | RETURN alpha; 237 | END; 238 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 239 | 240 | CREATE FUNCTION base32.zero_fill ( a int, b int ) RETURNS bigint AS $EOFCODE$ 241 | DECLARE 242 | bin text; 243 | m int; 244 | BEGIN 245 | 246 | IF (b >= 32 OR b < -32) THEN 247 | m = b/32; 248 | b = b-(m*32); 249 | END IF; 250 | 251 | IF (b < 0) THEN 252 | b = 32 + b; 253 | END IF; 254 | 255 | IF (b = 0) THEN 256 | return ((a>>1)&2147483647)*2::bigint+((a>>b)&1); 257 | END IF; 258 | 259 | IF (a < 0) THEN 260 | a = (a >> 1); 261 | a = a & 2147483647; -- 0x7fffffff 262 | a = a | 1073741824; -- 0x40000000 263 | a = (a >> (b - 1)); 264 | ELSE 265 | a = (a >> b); 266 | END IF; 267 | 268 | RETURN a; 269 | END; 270 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 271 | 272 | CREATE FUNCTION base32.valid ( input text ) RETURNS boolean AS $EOFCODE$ 273 | BEGIN 274 | IF (upper(input) ~* '^[A-Z2-7]+=*$') THEN 275 | RETURN true; 276 | END IF; 277 | RETURN false; 278 | END; 279 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 280 | 281 | CREATE FUNCTION base32.decode ( input text ) RETURNS text AS $EOFCODE$ 282 | DECLARE 283 | i int; 284 | arr int[]; 285 | output text[]; 286 | len int; 287 | num int; 288 | 289 | value int = 0; 290 | index int = 0; 291 | bits int = 0; 292 | BEGIN 293 | len = character_length(input); 294 | IF (len = 0) THEN 295 | RETURN ''; 296 | END IF; 297 | 298 | IF (NOT base32.valid(input)) THEN 299 | RAISE EXCEPTION 'INVALID_BASE32'; 300 | END IF; 301 | 302 | input = replace(input, '=', ''); 303 | input = upper(input); 304 | len = character_length(input); 305 | num = len * 5 / 8; 306 | 307 | select array(select * from generate_series(1,num)) 308 | INTO arr; 309 | 310 | FOR i IN 1 .. len LOOP 311 | value = (value << 5) | base32.base32_alphabet_to_decimal_int(substring(input from i for 1)); 312 | bits = bits + 5; 313 | IF (bits >= 8) THEN 314 | arr[index] = base32.zero_fill(value, (bits - 8)) & 255; -- arr[index] = (value >>> (bits - 8)) & 255; 315 | index = index + 1; 316 | bits = bits - 8; 317 | END IF; 318 | END LOOP; 319 | 320 | len = cardinality(arr); 321 | FOR i IN 0 .. len-2 LOOP 322 | output = array_append(output, chr(arr[i])); 323 | END LOOP; 324 | 325 | RETURN array_to_string(output, ''); 326 | END; 327 | $EOFCODE$ LANGUAGE plpgsql STABLE; -------------------------------------------------------------------------------- /extensions/@launchql/base32/test/__snapshots__/base32.decode.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`base32 INQXI 1`] = `"Cat"`; 4 | 5 | exports[`base32 MNUGK3LJON2HE6LJONTXEZLBOQ====== 1`] = `"chemistryisgreat"`; 6 | 7 | exports[`base32 MY====== 1`] = `"f"`; 8 | 9 | exports[`base32 MZXQ==== 1`] = `"fo"`; 10 | 11 | exports[`base32 MZXW6=== 1`] = `"foo"`; 12 | 13 | exports[`base32 MZXW6YQ= 1`] = `"foob"`; 14 | 15 | exports[`base32 MZXW6YTB 1`] = `"fooba"`; 16 | 17 | exports[`base32 MZXW6YTBOI====== 1`] = `"foobar"`; 18 | 19 | exports[`base32 case: 1 1`] = `""`; 20 | 21 | exports[`base32 mzxw6ytb 1`] = `"fooba"`; 22 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/test/__snapshots__/base32.encode.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`base32 case: 1 1`] = `""`; 4 | 5 | exports[`base32 f 1`] = `"MY======"`; 6 | 7 | exports[`base32 fo 1`] = `"MZXQ===="`; 8 | 9 | exports[`base32 foo 1`] = `"MZXW6==="`; 10 | 11 | exports[`base32 foob 1`] = `"MZXW6YQ="`; 12 | 13 | exports[`base32 fooba 1`] = `"MZXW6YTB"`; 14 | 15 | exports[`base32 foobar 1`] = `"MZXW6YTBOI======"`; 16 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/test/base32.decode.test.js: -------------------------------------------------------------------------------- 1 | import { getConnections } from './utils'; 2 | import cases from 'jest-in-case'; 3 | 4 | let db, base32, teardown; 5 | const objs = { 6 | tables: {} 7 | }; 8 | 9 | beforeAll(async () => { 10 | ({ db, teardown } = await getConnections()); 11 | base32 = db.helper('base32'); 12 | }); 13 | 14 | afterAll(async () => { 15 | try { 16 | //try catch here allows us to see the sql parsing issues! 17 | await teardown(); 18 | } catch (e) { 19 | // noop 20 | } 21 | }); 22 | 23 | beforeEach(async () => { 24 | await db.beforeEach(); 25 | }); 26 | 27 | afterEach(async () => { 28 | await db.afterEach(); 29 | }); 30 | 31 | it('base32_to_decimal', async () => { 32 | const [{ base32_to_decimal }] = await base32.call('base32_to_decimal', { 33 | input: 'INQXI===' 34 | }); 35 | expect(base32_to_decimal).toEqual([ 36 | '8', 37 | '13', 38 | '16', 39 | '23', 40 | '8', 41 | '=', 42 | '=', 43 | '=' 44 | ]); 45 | }); 46 | 47 | it('base32_to_decimal', async () => { 48 | const [{ base32_to_decimal }] = await base32.call('base32_to_decimal', { 49 | input: 'INQXI===' 50 | }); 51 | expect(base32_to_decimal).toEqual([ 52 | '8', 53 | '13', 54 | '16', 55 | '23', 56 | '8', 57 | '=', 58 | '=', 59 | '=' 60 | ]); 61 | }); 62 | 63 | it('decimal_to_chunks', async () => { 64 | const [{ decimal_to_chunks }] = await base32.call( 65 | 'decimal_to_chunks', 66 | { 67 | input: ['8', '13', '16', '23', '8', '=', '=', '='] 68 | }, 69 | { 70 | input: 'text[]' 71 | } 72 | ); 73 | expect(decimal_to_chunks).toEqual([ 74 | '01000', 75 | '01101', 76 | '10000', 77 | '10111', 78 | '01000', 79 | 'xxxxx', 80 | 'xxxxx', 81 | 'xxxxx' 82 | ]); 83 | }); 84 | 85 | it('decode', async () => { 86 | const [{ decode }] = await base32.call('decode', { 87 | input: 'INQXI' 88 | }); 89 | expect(decode).toEqual('Cat'); 90 | }); 91 | 92 | it('zero_fill', async () => { 93 | const [{ zero_fill }] = await base32.call('zero_fill', { 94 | a: 300, 95 | b: 2 96 | }); 97 | expect(zero_fill).toBe('75'); 98 | }); 99 | 100 | it('zero_fill (-)', async () => { 101 | const [{ zero_fill }] = await base32.call('zero_fill', { 102 | a: -300, 103 | b: 2 104 | }); 105 | expect(zero_fill).toBe('1073741749'); 106 | }); 107 | 108 | it('zero_fill (0)', async () => { 109 | const [{ zero_fill }] = await base32.call('zero_fill', { 110 | a: -300, 111 | b: 0 112 | }); 113 | expect(zero_fill).toBe('4294966996'); 114 | }); 115 | 116 | cases( 117 | 'base32', 118 | async (opts) => { 119 | const [result] = await base32.call('decode', { 120 | input: opts.name 121 | }); 122 | expect(result.decode).toEqual(opts.result); 123 | expect(result.decode).toMatchSnapshot(); 124 | }, 125 | [ 126 | { result: '', name: '' }, 127 | { result: 'Cat', name: 'INQXI' }, 128 | { result: 'chemistryisgreat', name: 'MNUGK3LJON2HE6LJONTXEZLBOQ======' }, 129 | { result: 'f', name: 'MY======' }, 130 | { result: 'fo', name: 'MZXQ====' }, 131 | { result: 'foo', name: 'MZXW6===' }, 132 | { result: 'foob', name: 'MZXW6YQ=' }, 133 | { result: 'fooba', name: 'MZXW6YTB' }, 134 | { result: 'fooba', name: 'mzxw6ytb' }, 135 | { result: 'foobar', name: 'MZXW6YTBOI======' } 136 | ] 137 | ); 138 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/test/base32.encode.test.js: -------------------------------------------------------------------------------- 1 | import { getConnections } from './utils'; 2 | import cases from 'jest-in-case'; 3 | 4 | let db, base32, teardown; 5 | const objs = { 6 | tables: {} 7 | }; 8 | 9 | beforeAll(async () => { 10 | ({ db, teardown } = await getConnections()); 11 | base32 = db.helper('base32'); 12 | }); 13 | 14 | afterAll(async () => { 15 | try { 16 | //try catch here allows us to see the sql parsing issues! 17 | await teardown(); 18 | } catch (e) { 19 | // noop 20 | } 21 | }); 22 | 23 | beforeEach(async () => { 24 | await db.beforeEach(); 25 | }); 26 | 27 | afterEach(async () => { 28 | await db.afterEach(); 29 | }); 30 | 31 | /* 32 | input: Cat (with a cap C) 33 | 34 | 35 | ASCII decimal values [67,97,116] 36 | Binary format [01000011, 01100001, 01110100] 37 | 38 | 39 | (BYTE = 8 bits) 40 | 41 | convert an input byte stream into group of 5 bytes 42 | if there are less than 5, adding padding 43 | 44 | [01000011, 01100001, 01110100, xxxxxxxx, xxxxxxxx] 45 | 46 | break these into 5 bit chunks (5 * 8 = 40 bits, when we 40/5 = 8 new elements of 5 bits each) 47 | 48 | [01000, 01101, 10000, 10111, 0100x, xxxxx, xxxxx, xxxxx] 49 | 50 | if a chunk has a mix of real bits (0|1) and empty (x), replace x with 0 51 | 52 | [01000, 01101, 10000, 10111, 01000, xxxxx, xxxxx, xxxxx] 53 | 54 | convert to decimal value 55 | 56 | [01000, 01101, 10000, 10111, 01000, xxxxx, xxxxx, xxxxx] 57 | [0b01000, 0b01101, 0b10000, 0b10111, 0b01000, xxxxx, xxxxx, xxxxx] 58 | 59 | convert to decimal value 60 | 61 | [ 8, 13, 16, 23, 8, '=', '=', '=' ] 62 | 63 | 64 | Table 3: The Base 32 Alphabet 65 | 66 | Value Encoding Value Encoding Value Encoding Value Encoding 67 | 0 A 9 J 18 S 27 3 68 | 1 B 10 K 19 T 28 4 69 | 2 C 11 L 20 U 29 5 70 | 3 D 12 M 21 V 30 6 71 | 4 E 13 N 22 W 31 7 72 | 5 F 14 O 23 X 73 | 6 G 15 P 24 Y (pad) = 74 | 7 H 16 Q 25 Z 75 | 8 I 17 R 26 2 76 | 77 | [ 8, 13, 16, 23, 8, '=', '=', '=' ] 78 | [ I, N, Q, X, I, '=', '=', '=' ] 79 | 80 | */ 81 | 82 | it('to_ascii', async () => { 83 | const [{ to_ascii }] = await base32.call('to_ascii', { 84 | input: 'Cat' 85 | }); 86 | expect(to_ascii).toEqual([67, 97, 116]); 87 | }); 88 | 89 | it('to_binary', async () => { 90 | const [{ to_ascii }] = await base32.call('to_ascii', { 91 | input: 'Cat' 92 | }); 93 | const [{ to_binary }] = await base32.call( 94 | 'to_binary', 95 | { 96 | input: to_ascii 97 | }, 98 | { 99 | input: 'int[]' 100 | } 101 | ); 102 | expect(to_binary).toEqual(['01000011', '01100001', '01110100']); 103 | }); 104 | 105 | it('to_groups', async () => { 106 | const [{ to_groups }] = await base32.call( 107 | 'to_groups', 108 | { 109 | input: ['01000011', '01100001', '01110100'] 110 | }, 111 | { 112 | input: 'text[]' 113 | } 114 | ); 115 | // [01000011, 01100001, 01110100, xxxxxxxx, xxxxxxxx] 116 | expect(to_groups).toEqual([ 117 | '01000011', 118 | '01100001', 119 | '01110100', 120 | 'xxxxxxxx', 121 | 'xxxxxxxx' 122 | ]); 123 | }); 124 | 125 | it('to_chunks', async () => { 126 | const [{ to_chunks }] = await base32.call( 127 | 'to_chunks', 128 | { 129 | input: ['01000011', '01100001', '01110100', 'xxxxxxxx', 'xxxxxxxx'] 130 | }, 131 | { 132 | input: 'text[]' 133 | } 134 | ); 135 | expect(to_chunks).toEqual([ 136 | '01000', 137 | '01101', 138 | '10000', 139 | '10111', 140 | '0100x', 141 | 'xxxxx', 142 | 'xxxxx', 143 | 'xxxxx' 144 | ]); 145 | }); 146 | 147 | it('fill_chunks', async () => { 148 | const [{ fill_chunks }] = await base32.call( 149 | 'fill_chunks', 150 | { 151 | input: [ 152 | '01000', 153 | '01101', 154 | '10000', 155 | '10111', 156 | '0100x', 157 | 'xxxxx', 158 | 'xxxxx', 159 | 'xxxxx' 160 | ] 161 | }, 162 | { 163 | input: 'text[]' 164 | } 165 | ); 166 | expect(fill_chunks).toEqual([ 167 | '01000', 168 | '01101', 169 | '10000', 170 | '10111', 171 | '01000', 172 | 'xxxxx', 173 | 'xxxxx', 174 | 'xxxxx' 175 | ]); 176 | }); 177 | 178 | it('to_decimal', async () => { 179 | const [{ to_decimal }] = await base32.call( 180 | 'to_decimal', 181 | { 182 | input: [ 183 | '01000', 184 | '01101', 185 | '10000', 186 | '10111', 187 | '01000', 188 | 'xxxxx', 189 | 'xxxxx', 190 | 'xxxxx' 191 | ] 192 | }, 193 | { 194 | input: 'text[]' 195 | } 196 | ); 197 | expect(to_decimal).toEqual(['8', '13', '16', '23', '8', '=', '=', '=']); 198 | }); 199 | 200 | it('to_base32', async () => { 201 | const [{ to_base32 }] = await base32.call( 202 | 'to_base32', 203 | { 204 | input: ['8', '13', '16', '23', '8', '=', '=', '='] 205 | }, 206 | { 207 | input: 'text[]' 208 | } 209 | ); 210 | expect(to_base32).toEqual('INQXI==='); 211 | }); 212 | 213 | cases( 214 | 'base32', 215 | async (opts) => { 216 | const [result] = await base32.call('encode', { 217 | input: opts.name 218 | }); 219 | expect(result.encode).toEqual(opts.result); 220 | expect(result.encode).toMatchSnapshot(); 221 | }, 222 | [ 223 | { name: '', result: '' }, 224 | { name: 'f', result: 'MY======' }, 225 | { name: 'fo', result: 'MZXQ====' }, 226 | { name: 'foo', result: 'MZXW6===' }, 227 | { name: 'foob', result: 'MZXW6YQ=' }, 228 | { name: 'fooba', result: 'MZXW6YTB' }, 229 | { name: 'foobar', result: 'MZXW6YTBOI======' } 230 | ] 231 | ); 232 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/test/utils/index.js: -------------------------------------------------------------------------------- 1 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 2 | export { getConnections } from '@launchql/db-testing'; 3 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/verify/schemas/base32/procedures/decode.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/base32/procedures/decode on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_function ('base32.decode'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/verify/schemas/base32/procedures/encode.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/base32/procedures/encode on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_function ('base32.encode'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /extensions/@launchql/base32/verify/schemas/base32/schema.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/base32/schema on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_schema ('base32'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ['/packages/*/jest.config.js'], 3 | coverageDirectory: '/coverage', 4 | transform: { 5 | '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.4.2", 3 | "useWorkspaces": true, 4 | "npmClient": "yarn", 5 | "npmClientArgs": [ 6 | "--no-lockfile" 7 | ], 8 | "packages": [ 9 | "packages/*" 10 | ], 11 | "version": "independent", 12 | "registry": "https://registry.npmjs.org", 13 | "command": { 14 | "create": { 15 | "license": "SEE LICENSE IN LICENSE", 16 | "access": "restricted" 17 | }, 18 | "publish": { 19 | "allowBranch": "master" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "totp", 4 | "scripts": { 5 | "build": "lerna run prepare --parallel", 6 | "bootstrap": "lerna bootstrap --use-workspaces" 7 | }, 8 | "devDependencies": { 9 | "@babel/cli": "7.12.1", 10 | "@babel/core": "7.12.3", 11 | "@pyramation/babel-preset-env": "0.1.0", 12 | "babel-eslint": "10.1.0", 13 | "babel-jest": "26.6.1", 14 | "eslint": "7.12.1", 15 | "eslint-config-prettier": "^6.10.0", 16 | "eslint-plugin-prettier": "^3.1.2", 17 | "jest": "26.6.1", 18 | "lerna": "3.22.1", 19 | "prettier": "2.1.2" 20 | }, 21 | "workspaces": [ 22 | "packages/*" 23 | ] 24 | } -------------------------------------------------------------------------------- /packages/totp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-class-properties", 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-proposal-export-default-from", 6 | "@babel/plugin-transform-runtime", 7 | "import-graphql" 8 | ], 9 | "presets": [ 10 | "@babel/env" 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/totp/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /packages/totp/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | main/ 4 | module/ 5 | coverage/ -------------------------------------------------------------------------------- /packages/totp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier'], 3 | extends: ['eslint:recommended', 'prettier'], 4 | parser: 'babel-eslint', 5 | globals: { 6 | jasmine: true 7 | }, 8 | parserOptions: { 9 | ecmaVersion: 11, 10 | sourceType: 'module', 11 | ecmaFeatures: { 12 | jsx: true 13 | } 14 | }, 15 | env: { 16 | es6: true, 17 | browser: true, 18 | node: true, 19 | jest: true 20 | }, 21 | rules: { 22 | 'no-debugger': 2, 23 | 'no-alert': 2, 24 | 'no-await-in-loop': 0, 25 | 'no-prototype-builtins': 0, 26 | 'no-return-assign': ['error', 'except-parens'], 27 | 'no-restricted-syntax': [ 28 | 2, 29 | 'ForInStatement', 30 | 'LabeledStatement', 31 | 'WithStatement' 32 | ], 33 | 'no-unused-vars': [ 34 | 0, 35 | { 36 | ignoreSiblings: true, 37 | argsIgnorePattern: 'React|res|next|^_' 38 | } 39 | ], 40 | 'prefer-const': [ 41 | 'error', 42 | { 43 | destructuring: 'all' 44 | } 45 | ], 46 | 'no-unused-expressions': [ 47 | 2, 48 | { 49 | allowTaggedTemplates: true 50 | } 51 | ], 52 | 'no-console': 1, 53 | 'comma-dangle': 2, 54 | 'jsx-quotes': [2, 'prefer-double'], 55 | 'linebreak-style': ['error', 'unix'], 56 | quotes: [ 57 | 2, 58 | 'single', 59 | { 60 | avoidEscape: true, 61 | allowTemplateLiterals: true 62 | } 63 | ], 64 | 'prettier/prettier': [ 65 | 'error', 66 | { 67 | trailingComma: 'none', 68 | singleQuote: true, 69 | printWidth: 80 70 | } 71 | ] 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /packages/totp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/totp/.npmignore: -------------------------------------------------------------------------------- 1 | # NOTE keeping this minimal since we generally want everything 2 | 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # npm package lock 10 | package-lock.json 11 | yarn.lock 12 | -------------------------------------------------------------------------------- /packages/totp/.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true -------------------------------------------------------------------------------- /packages/totp/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Web, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/totp/Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION = launchql-totp 2 | DATA = sql/launchql-totp--0.0.3.sql 3 | 4 | PG_CONFIG = pg_config 5 | PGXS := $(shell $(PG_CONFIG) --pgxs) 6 | include $(PGXS) 7 | -------------------------------------------------------------------------------- /packages/totp/deploy/schemas/totp/procedures/generate_totp.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/totp/procedures/generate_totp to pg 2 | -- requires: schemas/totp/schema 3 | -- requires: schemas/totp/procedures/urlencode 4 | 5 | BEGIN; 6 | 7 | -- https://www.youtube.com/watch?v=VOYxF12K1vE 8 | -- https://tools.ietf.org/html/rfc6238 9 | -- http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/ 10 | -- https://gist.github.com/bwbroersma/676d0de32263ed554584ab132434ebd9 11 | 12 | CREATE FUNCTION totp.pad_secret ( 13 | input bytea, 14 | len int 15 | ) returns bytea as $$ 16 | DECLARE 17 | output bytea; 18 | orig_length int = octet_length(input); 19 | BEGIN 20 | IF (orig_length = len) THEN 21 | RETURN input; 22 | END IF; 23 | 24 | -- create blank bytea size of new length 25 | output = lpad('', len, 'x')::bytea; 26 | 27 | FOR i IN 0 .. len-1 LOOP 28 | output = set_byte(output, i, get_byte(input, i % orig_length)); 29 | END LOOP; 30 | 31 | RETURN output; 32 | END; 33 | $$ 34 | LANGUAGE 'plpgsql' IMMUTABLE; 35 | 36 | CREATE FUNCTION totp.base32_to_hex ( 37 | input text 38 | ) returns text as $$ 39 | DECLARE 40 | output text[]; 41 | decoded text = base32.decode(input); 42 | len int = character_length(decoded); 43 | hx text; 44 | BEGIN 45 | 46 | FOR i IN 1 .. len LOOP 47 | hx = to_hex(ascii(substring(decoded from i for 1)))::text; 48 | IF (character_length(hx) = 1) THEN 49 | -- if it is odd number of digits, pad a 0 so it can later 50 | hx = '0' || hx; 51 | END IF; 52 | output = array_append(output, hx); 53 | END LOOP; 54 | 55 | RETURN array_to_string(output, ''); 56 | END; 57 | $$ 58 | LANGUAGE 'plpgsql' IMMUTABLE; 59 | 60 | CREATE FUNCTION totp.hotp(key BYTEA, c INT, digits INT DEFAULT 6, hash TEXT DEFAULT 'sha1') RETURNS TEXT AS $$ 61 | DECLARE 62 | c BYTEA := '\x' || LPAD(TO_HEX(c), 16, '0'); 63 | mac BYTEA := HMAC(c, key, hash); 64 | trunc_offset INT := GET_BYTE(mac, length(mac) - 1) % 16; 65 | result TEXT := SUBSTRING(SET_BIT(SUBSTRING(mac FROM 1 + trunc_offset FOR 4), 7, 0)::TEXT, 2)::BIT(32)::INT % (10 ^ digits)::INT; 66 | BEGIN 67 | RETURN LPAD(result, digits, '0'); 68 | END; 69 | $$ LANGUAGE plpgsql IMMUTABLE; 70 | 71 | CREATE FUNCTION totp.generate( 72 | secret text, 73 | period int DEFAULT 30, 74 | digits int DEFAULT 6, 75 | time_from timestamptz DEFAULT NOW(), 76 | hash text DEFAULT 'sha1', 77 | encoding text DEFAULT 'base32', 78 | clock_offset int DEFAULT 0 79 | ) RETURNS text AS $$ 80 | DECLARE 81 | c int := FLOOR(EXTRACT(EPOCH FROM time_from) / period)::int + clock_offset; 82 | key bytea; 83 | BEGIN 84 | 85 | IF (encoding = 'base32') THEN 86 | key = ( '\x' || totp.base32_to_hex(secret) )::bytea; 87 | ELSE 88 | key = secret::bytea; 89 | END IF; 90 | 91 | RETURN totp.hotp(key, c, digits, hash); 92 | END; 93 | $$ LANGUAGE plpgsql STABLE; 94 | 95 | CREATE FUNCTION totp.verify ( 96 | secret text, 97 | check_totp text, 98 | period int default 30, 99 | digits int default 6, 100 | time_from timestamptz DEFAULT NOW(), 101 | hash text default 'sha1', 102 | encoding text DEFAULT 'base32', 103 | clock_offset int default 0 104 | ) 105 | RETURNS boolean 106 | AS $$ 107 | SELECT totp.generate ( 108 | secret, 109 | period, 110 | digits, 111 | time_from, 112 | hash, 113 | encoding, 114 | clock_offset) = check_totp; 115 | $$ 116 | LANGUAGE 'sql'; 117 | 118 | CREATE FUNCTION totp.url (email text, totp_secret text, totp_interval int, totp_issuer text) 119 | RETURNS text 120 | AS $$ 121 | SELECT 122 | concat('otpauth://totp/', totp.urlencode (email), '?secret=', totp.urlencode (totp_secret), '&period=', totp.urlencode (totp_interval::text), '&issuer=', totp.urlencode (totp_issuer)); 123 | $$ 124 | LANGUAGE 'sql' 125 | STRICT IMMUTABLE; 126 | 127 | COMMIT; 128 | 129 | -------------------------------------------------------------------------------- /packages/totp/deploy/schemas/totp/procedures/random_base32.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/totp/procedures/random_base32 to pg 2 | -- requires: schemas/totp/schema 3 | 4 | BEGIN; 5 | 6 | CREATE FUNCTION totp.random_base32 (_length int DEFAULT 20) 7 | RETURNS text 8 | LANGUAGE sql 9 | AS $$ 10 | SELECT 11 | string_agg(('{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,2,3,4,5,6,7}'::text[])[ceil(random() * 32)], '') 12 | FROM 13 | generate_series(1, _length); 14 | $$; 15 | 16 | CREATE FUNCTION totp.generate_secret(hash TEXT DEFAULT 'sha1') RETURNS BYTEA AS $$ 17 | BEGIN 18 | -- See https://tools.ietf.org/html/rfc4868#section-2.1.2 19 | -- The optimal key length for HMAC is the block size of the algorithm 20 | CASE 21 | WHEN hash = 'sha1' THEN RETURN totp.random_base32(20); -- = 160 bits 22 | WHEN hash = 'sha256' THEN RETURN totp.random_base32(32); -- = 256 bits 23 | WHEN hash = 'sha512' THEN RETURN totp.random_base32(64); -- = 512 bits 24 | ELSE 25 | RAISE EXCEPTION 'Unsupported hash algorithm for OTP (see RFC6238/4226).'; 26 | RETURN NULL; 27 | END CASE; 28 | END; 29 | $$ LANGUAGE plpgsql VOLATILE; 30 | 31 | COMMIT; 32 | 33 | -------------------------------------------------------------------------------- /packages/totp/deploy/schemas/totp/procedures/urlencode.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/totp/procedures/urlencode to pg 2 | -- requires: schemas/totp/schema 3 | 4 | -- https://stackoverflow.com/questions/10318014/javascript-encodeuri-like-function-in-postgresql/40762846 5 | BEGIN; 6 | CREATE FUNCTION totp.urlencode (in_str text) 7 | RETURNS text 8 | AS $$ 9 | DECLARE 10 | _i int4; 11 | _temp varchar; 12 | _ascii int4; 13 | _result text := ''; 14 | BEGIN 15 | FOR _i IN 1..length(in_str) 16 | LOOP 17 | _temp := substr(in_str, _i, 1); 18 | IF _temp ~ '[0-9a-zA-Z:/@._?#-]+' THEN 19 | _result := _result || _temp; 20 | ELSE 21 | _ascii := ascii(_temp); 22 | IF _ascii > x'07ff'::int4 THEN 23 | RAISE exception 'won''t deal with 3 (or more) byte sequences.'; 24 | END IF; 25 | IF _ascii <= x'07f'::int4 THEN 26 | _temp := '%' || to_hex(_ascii); 27 | ELSE 28 | _temp := '%' || to_hex((_ascii & x'03f'::int4) + x'80'::int4); 29 | _ascii := _ascii >> 6; 30 | _temp := '%' || to_hex((_ascii & x'01f'::int4) + x'c0'::int4) || _temp; 31 | END IF; 32 | _result := _result || upper(_temp); 33 | END IF; 34 | END LOOP; 35 | RETURN _result; 36 | END; 37 | $$ 38 | LANGUAGE 'plpgsql' 39 | STRICT IMMUTABLE 40 | ; 41 | 42 | COMMIT; 43 | 44 | -------------------------------------------------------------------------------- /packages/totp/deploy/schemas/totp/schema.sql: -------------------------------------------------------------------------------- 1 | -- Deploy schemas/totp/schema to pg 2 | 3 | 4 | BEGIN; 5 | 6 | CREATE SCHEMA totp; 7 | 8 | COMMIT; 9 | -------------------------------------------------------------------------------- /packages/totp/launchql-totp.control: -------------------------------------------------------------------------------- 1 | # launchql-totp extension 2 | comment = 'launchql-totp extension' 3 | default_version = '0.0.3' 4 | module_pathname = '$libdir/launchql-totp' 5 | requires = 'pgcrypto,plpgsql,launchql-base32' 6 | relocatable = false 7 | superuser = false 8 | -------------------------------------------------------------------------------- /packages/totp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@launchql/totp", 3 | "version": "0.0.3", 4 | "description": "totp", 5 | "author": "Web, Inc. ", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "test": "FAST_TEST=1 launchql-templatedb && jest", 11 | "test:watch": "FAST_TEST=1 jest --watch" 12 | }, 13 | "devDependencies": { 14 | "@babel/cli": "7.10.4", 15 | "@babel/core": "7.10.4", 16 | "@babel/plugin-proposal-class-properties": "7.10.4", 17 | "@babel/plugin-proposal-export-default-from": "7.10.4", 18 | "@babel/plugin-proposal-object-rest-spread": "7.10.4", 19 | "@babel/plugin-transform-runtime": "7.10.4", 20 | "@babel/preset-env": "7.10.4", 21 | "@babel/runtime": "^7.4.2", 22 | "@launchql/db-testing": "latest", 23 | "babel-eslint": "10.1.0", 24 | "babel-jest": "26.1.0", 25 | "babel-plugin-import-graphql": "2.7.0", 26 | "babel-plugin-macros": "2.8.0", 27 | "eslint": "^7.3.1", 28 | "eslint-config-prettier": "^6.10.0", 29 | "eslint-plugin-prettier": "^3.1.2", 30 | "graphql": "^14.0.2", 31 | "graphql-tag": "2.10.3", 32 | "jest": "26.1.0", 33 | "jest-in-case": "1.0.2", 34 | "prettier": "2.0.5", 35 | "regenerator-runtime": "^0.13.2", 36 | "speakeasy": "2.0.0" 37 | }, 38 | "dependencies": { 39 | "@launchql/base32": "^0.0.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/totp/readme.md: -------------------------------------------------------------------------------- 1 | # totp [![Build Status](https://travis-ci.com/pyramation/totp.svg?branch=master)](https://travis-ci.com/pyramation/totp) 2 | 3 | TOTP implementation in pure PostgreSQL plpgsql 4 | 5 | This extension provides the HMAC Time-Based One-Time Password Algorithm (TOTP) as specfied in RFC 4226 as pure plpgsql functions. 6 | 7 | # Usage 8 | 9 | ## totp.generate 10 | 11 | ```sql 12 | SELECT totp.generate('mysecret'); 13 | 14 | -- you can also specify totp_interval, and totp_length 15 | SELECT totp.generate('mysecret', 30, 6); 16 | ``` 17 | 18 | In this case, produces a TOTP code of length 6 19 | 20 | ``` 21 | 013438 22 | ``` 23 | 24 | ## totp.verify 25 | 26 | ```sql 27 | SELECT totp.verify('mysecret', '765430'); 28 | 29 | -- you can also specify totp_interval, and totp_length 30 | SELECT totp.verify('mysecret', '765430', 30, 6); 31 | ``` 32 | 33 | Depending on input, returns `TRUE/FALSE` 34 | 35 | ## totp.url 36 | 37 | ```sql 38 | -- totp.url ( email text, totp_secret text, totp_interval int, totp_issuer text ) 39 | SELECT totp.url( 40 | 'customer@email.com', 41 | 'mysecret', 42 | 30, 43 | 'Acme Inc' 44 | ); 45 | ``` 46 | 47 | Will produce a URL-encoded string 48 | 49 | ``` 50 | otpauth://totp/customer@email.com?secret=mysecret&period=30&issuer=Acme%20Inc 51 | ``` 52 | 53 | # caveats 54 | 55 | Currently only supports `sha1`, pull requests welcome! 56 | 57 | # credits 58 | 59 | Thanks to 60 | 61 | https://tools.ietf.org/html/rfc6238 62 | 63 | https://www.youtube.com/watch?v=VOYxF12K1vE 64 | 65 | https://pgxn.org/dist/otp/ 66 | 67 | # Development 68 | 69 | ## start the postgres db process 70 | 71 | First you'll want to start the postgres docker (you can also just use `docker-compose up -d`): 72 | 73 | ```sh 74 | make up 75 | ``` 76 | 77 | ## install modules 78 | 79 | Install modules 80 | 81 | ```sh 82 | yarn install 83 | ``` 84 | 85 | ## install the Postgres extensions 86 | 87 | Now that the postgres process is running, install the extensions: 88 | 89 | ```sh 90 | make install 91 | ``` 92 | 93 | This basically `ssh`s into the postgres instance with the `packages/` folder mounted as a volume, and installs the bundled sql code as pgxn extensions. 94 | 95 | ## testing 96 | 97 | Testing will load all your latest sql changes and create fresh, populated databases for each sqitch module in `packages/`. 98 | 99 | ```sh 100 | yarn test:watch 101 | ``` 102 | 103 | ## building new modules 104 | 105 | Create a new folder in `packages/` 106 | 107 | ```sh 108 | lql init 109 | ``` 110 | 111 | Then, run a generator: 112 | 113 | ```sh 114 | lql generate 115 | ``` 116 | 117 | You can also add arguments if you already know what you want to do: 118 | 119 | ```sh 120 | lql generate schema --schema myschema 121 | lql generate table --schema myschema --table mytable 122 | ``` 123 | 124 | ## deploy code as extensions 125 | 126 | `cd` into `packages/`, and run `lql package`. This will make an sql file in `packages//sql/` used for `CREATE EXTENSION` calls to install your sqitch module as an extension. 127 | 128 | ## recursive deploy 129 | 130 | You can also deploy all modules utilizing versioning as sqtich modules. Remove `--createdb` if you already created your db: 131 | 132 | ```sh 133 | lql deploy awesome-db --yes --recursive --createdb 134 | ``` 135 | -------------------------------------------------------------------------------- /packages/totp/revert/schemas/totp/procedures/generate_totp.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/totp/procedures/generate_totp from pg 2 | 3 | BEGIN; 4 | 5 | -- DROP FUNCTION totp.generate_totp; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /packages/totp/revert/schemas/totp/procedures/random_base32.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/totp/procedures/random_base32 from pg 2 | 3 | BEGIN; 4 | 5 | DROP FUNCTION totp.random_base32; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /packages/totp/revert/schemas/totp/procedures/urlencode.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/totp/procedures/urlencode from pg 2 | 3 | BEGIN; 4 | 5 | DROP FUNCTION totp.urlencode; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /packages/totp/revert/schemas/totp/schema.sql: -------------------------------------------------------------------------------- 1 | -- Revert schemas/totp/schema from pg 2 | 3 | BEGIN; 4 | 5 | DROP SCHEMA totp; 6 | 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /packages/totp/sqitch.conf: -------------------------------------------------------------------------------- 1 | [core] 2 | engine = pg 3 | # plan_file = sqitch.plan 4 | # top_dir = . 5 | # [engine "pg"] 6 | # target = db:pg: 7 | # registry = sqitch 8 | # client = psql 9 | -------------------------------------------------------------------------------- /packages/totp/sqitch.plan: -------------------------------------------------------------------------------- 1 | %syntax-version=1.0.0 2 | %project=launchql-totp 3 | %uri=launchql-totp 4 | 5 | schemas/totp/schema [launchql-base32:schemas/base32/procedures/decode] 2017-08-11T08:11:51Z skitch # add schemas/totp/schema 6 | schemas/totp/procedures/urlencode [schemas/totp/schema] 2017-08-11T08:11:51Z skitch # add schemas/totp/procedures/urlencode 7 | schemas/totp/procedures/generate_totp [schemas/totp/schema schemas/totp/procedures/urlencode] 2017-08-11T08:11:51Z skitch # add schemas/totp/procedures/generate_totp 8 | schemas/totp/procedures/random_base32 [schemas/totp/schema] 2017-08-11T08:11:51Z skitch # add schemas/totp/procedures/random_base32 -------------------------------------------------------------------------------- /packages/totp/sql/launchql-totp--0.0.3.sql: -------------------------------------------------------------------------------- 1 | \echo Use "CREATE EXTENSION launchql-totp" to load this file. \quit 2 | CREATE SCHEMA totp; 3 | 4 | CREATE FUNCTION totp.urlencode ( in_str text ) RETURNS text AS $EOFCODE$ 5 | DECLARE 6 | _i int4; 7 | _temp varchar; 8 | _ascii int4; 9 | _result text := ''; 10 | BEGIN 11 | FOR _i IN 1..length(in_str) 12 | LOOP 13 | _temp := substr(in_str, _i, 1); 14 | IF _temp ~ '[0-9a-zA-Z:/@._?#-]+' THEN 15 | _result := _result || _temp; 16 | ELSE 17 | _ascii := ascii(_temp); 18 | IF _ascii > x'07ff'::int4 THEN 19 | RAISE exception 'won''t deal with 3 (or more) byte sequences.'; 20 | END IF; 21 | IF _ascii <= x'07f'::int4 THEN 22 | _temp := '%' || to_hex(_ascii); 23 | ELSE 24 | _temp := '%' || to_hex((_ascii & x'03f'::int4) + x'80'::int4); 25 | _ascii := _ascii >> 6; 26 | _temp := '%' || to_hex((_ascii & x'01f'::int4) + x'c0'::int4) || _temp; 27 | END IF; 28 | _result := _result || upper(_temp); 29 | END IF; 30 | END LOOP; 31 | RETURN _result; 32 | END; 33 | $EOFCODE$ LANGUAGE plpgsql STRICT IMMUTABLE; 34 | 35 | CREATE FUNCTION totp.pad_secret ( input bytea, len int ) RETURNS bytea AS $EOFCODE$ 36 | DECLARE 37 | output bytea; 38 | orig_length int = octet_length(input); 39 | BEGIN 40 | IF (orig_length = len) THEN 41 | RETURN input; 42 | END IF; 43 | 44 | -- create blank bytea size of new length 45 | output = lpad('', len, 'x')::bytea; 46 | 47 | FOR i IN 0 .. len-1 LOOP 48 | output = set_byte(output, i, get_byte(input, i % orig_length)); 49 | END LOOP; 50 | 51 | RETURN output; 52 | END; 53 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 54 | 55 | CREATE FUNCTION totp.base32_to_hex ( input text ) RETURNS text AS $EOFCODE$ 56 | DECLARE 57 | output text[]; 58 | decoded text = base32.decode(input); 59 | len int = character_length(decoded); 60 | hx text; 61 | BEGIN 62 | 63 | FOR i IN 1 .. len LOOP 64 | hx = to_hex(ascii(substring(decoded from i for 1)))::text; 65 | IF (character_length(hx) = 1) THEN 66 | -- if it is odd number of digits, pad a 0 so it can later 67 | hx = '0' || hx; 68 | END IF; 69 | output = array_append(output, hx); 70 | END LOOP; 71 | 72 | RETURN array_to_string(output, ''); 73 | END; 74 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 75 | 76 | CREATE FUNCTION totp.hotp ( key bytea, c int, digits int DEFAULT 6, hash text DEFAULT 'sha1' ) RETURNS text AS $EOFCODE$ 77 | DECLARE 78 | c BYTEA := '\x' || LPAD(TO_HEX(c), 16, '0'); 79 | mac BYTEA := HMAC(c, key, hash); 80 | trunc_offset INT := GET_BYTE(mac, length(mac) - 1) % 16; 81 | result TEXT := SUBSTRING(SET_BIT(SUBSTRING(mac FROM 1 + trunc_offset FOR 4), 7, 0)::TEXT, 2)::BIT(32)::INT % (10 ^ digits)::INT; 82 | BEGIN 83 | RETURN LPAD(result, digits, '0'); 84 | END; 85 | $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; 86 | 87 | CREATE FUNCTION totp.generate ( secret text, period int DEFAULT 30, digits int DEFAULT 6, time_from timestamptz DEFAULT now(), hash text DEFAULT 'sha1', encoding text DEFAULT 'base32', clock_offset int DEFAULT 0 ) RETURNS text AS $EOFCODE$ 88 | DECLARE 89 | c int := FLOOR(EXTRACT(EPOCH FROM time_from) / period)::int + clock_offset; 90 | key bytea; 91 | BEGIN 92 | 93 | IF (encoding = 'base32') THEN 94 | key = ( '\x' || totp.base32_to_hex(secret) )::bytea; 95 | ELSE 96 | key = secret::bytea; 97 | END IF; 98 | 99 | RETURN totp.hotp(key, c, digits, hash); 100 | END; 101 | $EOFCODE$ LANGUAGE plpgsql STABLE; 102 | 103 | CREATE FUNCTION totp.verify ( secret text, check_totp text, period int DEFAULT 30, digits int DEFAULT 6, time_from timestamptz DEFAULT now(), hash text DEFAULT 'sha1', encoding text DEFAULT 'base32', clock_offset int DEFAULT 0 ) RETURNS boolean AS $EOFCODE$ 104 | SELECT totp.generate ( 105 | secret, 106 | period, 107 | digits, 108 | time_from, 109 | hash, 110 | encoding, 111 | clock_offset) = check_totp; 112 | $EOFCODE$ LANGUAGE sql; 113 | 114 | CREATE FUNCTION totp.url ( email text, totp_secret text, totp_interval int, totp_issuer text ) RETURNS text AS $EOFCODE$ 115 | SELECT 116 | concat('otpauth://totp/', totp.urlencode (email), '?secret=', totp.urlencode (totp_secret), '&period=', totp.urlencode (totp_interval::text), '&issuer=', totp.urlencode (totp_issuer)); 117 | $EOFCODE$ LANGUAGE sql STRICT IMMUTABLE; 118 | 119 | CREATE FUNCTION totp.random_base32 ( _length int DEFAULT 20 ) RETURNS text LANGUAGE sql AS $EOFCODE$ 120 | SELECT 121 | string_agg(('{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,2,3,4,5,6,7}'::text[])[ceil(random() * 32)], '') 122 | FROM 123 | generate_series(1, _length); 124 | $EOFCODE$; 125 | 126 | CREATE FUNCTION totp.generate_secret ( hash text DEFAULT 'sha1' ) RETURNS bytea AS $EOFCODE$ 127 | BEGIN 128 | -- See https://tools.ietf.org/html/rfc4868#section-2.1.2 129 | -- The optimal key length for HMAC is the block size of the algorithm 130 | CASE 131 | WHEN hash = 'sha1' THEN RETURN totp.random_base32(20); -- = 160 bits 132 | WHEN hash = 'sha256' THEN RETURN totp.random_base32(32); -- = 256 bits 133 | WHEN hash = 'sha512' THEN RETURN totp.random_base32(64); -- = 512 bits 134 | ELSE 135 | RAISE EXCEPTION 'Unsupported hash algorithm for OTP (see RFC6238/4226).'; 136 | RETURN NULL; 137 | END CASE; 138 | END; 139 | $EOFCODE$ LANGUAGE plpgsql VOLATILE; -------------------------------------------------------------------------------- /packages/totp/test/__snapshots__/algo.totp.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`issue case: 1 1`] = `"476240"`; 4 | 5 | exports[`issue case: 2 1`] = `"788648"`; 6 | 7 | exports[`issue case: 3 1`] = `"080176"`; 8 | 9 | exports[`rfc6238 case: 1 1`] = `"94287082"`; 10 | 11 | exports[`rfc6238 case: 2 1`] = `"07081804"`; 12 | 13 | exports[`rfc6238 case: 3 1`] = `"14050471"`; 14 | 15 | exports[`rfc6238 case: 4 1`] = `"89005924"`; 16 | 17 | exports[`rfc6238 case: 5 1`] = `"69279037"`; 18 | 19 | exports[`rfc6238 case: 6 1`] = `"65353130"`; 20 | 21 | exports[`speakeasy test case: 1 1`] = `"287082"`; 22 | 23 | exports[`speakeasy test case: 2 1`] = `"081804"`; 24 | 25 | exports[`speakeasy test case: 3 1`] = `"360094"`; 26 | -------------------------------------------------------------------------------- /packages/totp/test/algo.totp.test.js: -------------------------------------------------------------------------------- 1 | import { getConnections } from './utils'; 2 | import cases from 'jest-in-case'; 3 | 4 | let db, totp, teardown; 5 | const objs = { 6 | tables: {} 7 | }; 8 | 9 | beforeAll(async () => { 10 | ({ db, teardown } = await getConnections()); 11 | totp = db.helper('totp'); 12 | }); 13 | 14 | afterAll(async () => { 15 | try { 16 | //try catch here allows us to see the sql parsing issues! 17 | await teardown(); 18 | } catch (e) { 19 | // noop 20 | } 21 | }); 22 | 23 | beforeEach(async () => { 24 | await db.beforeEach(); 25 | }); 26 | 27 | afterEach(async () => { 28 | await db.afterEach(); 29 | }); 30 | 31 | cases( 32 | 'rfc6238', 33 | async (opts) => { 34 | const { generate } = await db.one( 35 | ` 36 | SELECT totp.generate( 37 | secret := $1, 38 | period := 30, 39 | digits := $2, 40 | time_from := $3, 41 | hash := $4, 42 | encoding := NULL 43 | ) 44 | `, 45 | ['12345678901234567890', opts.len, opts.date, opts.algo] 46 | ); 47 | expect(generate).toEqual(opts.result); 48 | expect(generate).toMatchSnapshot(); 49 | }, 50 | [ 51 | // https://tools.ietf.org/html/rfc6238 52 | { 53 | date: '1970-01-01 00:00:59', 54 | len: 8, 55 | algo: 'sha1', 56 | result: '94287082' 57 | }, 58 | { 59 | date: '2005-03-18 01:58:29', 60 | len: 8, 61 | algo: 'sha1', 62 | result: '07081804' 63 | }, 64 | { 65 | date: '2005-03-18 01:58:31', 66 | len: 8, 67 | algo: 'sha1', 68 | result: '14050471' 69 | }, 70 | { 71 | date: '2009-02-13 23:31:30', 72 | len: 8, 73 | algo: 'sha1', 74 | result: '89005924' 75 | }, 76 | { 77 | date: '2033-05-18 03:33:20', 78 | len: 8, 79 | algo: 'sha1', 80 | result: '69279037' 81 | }, 82 | { 83 | date: '2603-10-11 11:33:20', 84 | len: 8, 85 | algo: 'sha1', 86 | result: '65353130' 87 | } 88 | ] 89 | ); 90 | 91 | // cases( 92 | // 'rfc6238 sha256', 93 | // async (opts) => { 94 | // const { generate } = await db.one( 95 | // ` 96 | // SELECT totp.generate( 97 | // secret := $1, 98 | // period := 30, 99 | // digits := $2, 100 | // time_from := $3, 101 | // algo := $4 102 | // ) 103 | // `, 104 | // ['12345678901234567890', opts.len, opts.date, opts.algo] 105 | // ); 106 | // expect(generate).toEqual(opts.result); 107 | // expect(generate).toMatchSnapshot(); 108 | // }, 109 | // [ 110 | // // https://tools.ietf.org/html/rfc6238 111 | // { 112 | // date: '1970-01-01 00:00:59', 113 | // len: 8, 114 | // algo: 'sha256', 115 | // result: '94287082' 116 | // }, 117 | // { 118 | // date: '2005-03-18 01:58:29', 119 | // len: 8, 120 | // algo: 'sha256', 121 | // result: '07081804' 122 | // }, 123 | // { 124 | // date: '2005-03-18 01:58:31', 125 | // len: 8, 126 | // algo: 'sha256', 127 | // result: '14050471' 128 | // }, 129 | // { 130 | // date: '2009-02-13 23:31:30', 131 | // len: 8, 132 | // algo: 'sha256', 133 | // result: '89005924' 134 | // }, 135 | // { 136 | // date: '2033-05-18 03:33:20', 137 | // len: 8, 138 | // algo: 'sha256', 139 | // result: '69279037' 140 | // }, 141 | // { 142 | // date: '2603-10-11 11:33:20', 143 | // len: 8, 144 | // algo: 'sha256', 145 | // result: '65353130' 146 | // } 147 | // ] 148 | // ); 149 | 150 | cases( 151 | 'speakeasy test', 152 | async (opts) => { 153 | const { generate } = await db.one( 154 | ` 155 | SELECT totp.generate( 156 | secret := $1, 157 | period := $5, 158 | digits := $2, 159 | time_from := $3, 160 | hash := $4, 161 | encoding := NULL 162 | ) 163 | `, 164 | ['12345678901234567890', opts.len, opts.date, opts.algo, opts.step] 165 | ); 166 | expect(generate).toEqual(opts.result); 167 | expect(generate).toMatchSnapshot(); 168 | }, 169 | [ 170 | // https://github.com/speakeasyjs/speakeasy/blob/master/test/totp_test.js 171 | { 172 | date: '1970-01-01 00:00:59', 173 | len: 6, 174 | step: 30, 175 | algo: 'sha1', 176 | result: '287082' 177 | }, 178 | { 179 | date: '2005-03-18 01:58:29', 180 | len: 6, 181 | step: 30, 182 | algo: 'sha1', 183 | result: '081804' 184 | }, 185 | { 186 | date: '2005-03-18 01:58:29', 187 | len: 6, 188 | step: 60, // 60 seconds! 189 | algo: 'sha1', 190 | result: '360094' 191 | } 192 | // { 193 | // date: '2009-02-13 23:31:30', 194 | // len: 8, 195 | // algo: 'sha1', 196 | // result: '89005924' 197 | // }, 198 | // { 199 | // date: '2033-05-18 03:33:20', 200 | // len: 8, 201 | // algo: 'sha1', 202 | // result: '69279037' 203 | // }, 204 | // { 205 | // date: '2603-10-11 11:33:20', 206 | // len: 8, 207 | // algo: 'sha1', 208 | // result: '65353130' 209 | // } 210 | ] 211 | ); 212 | 213 | cases( 214 | 'verify', 215 | async (opts) => { 216 | const [{ verified }] = await db.any( 217 | `SELECT * FROM totp.verify( 218 | secret := $1, 219 | check_totp := $2, 220 | period := $3, 221 | digits := $4, 222 | time_from := $5, 223 | encoding := NULL 224 | ) as verified`, 225 | ['12345678901234567890', opts.result, opts.step, opts.len, opts.date] 226 | ); 227 | expect(verified).toBe(true); 228 | }, 229 | [ 230 | // https://github.com/speakeasyjs/speakeasy/blob/master/test/totp_test.js 231 | { 232 | date: '1970-01-01 00:00:59', 233 | len: 6, 234 | step: 30, 235 | algo: 'sha1', 236 | result: '287082' 237 | }, 238 | { 239 | date: '2005-03-18 01:58:29', 240 | len: 6, 241 | step: 30, 242 | algo: 'sha1', 243 | result: '081804' 244 | }, 245 | { 246 | date: '2005-03-18 01:58:29', 247 | len: 6, 248 | step: 60, // 60 seconds! 249 | algo: 'sha1', 250 | result: '360094' 251 | }, 252 | { 253 | date: '1970-01-01 00:00:59', 254 | len: 8, 255 | step: 30, 256 | algo: 'sha1', 257 | result: '94287082' 258 | }, 259 | { 260 | date: '2005-03-18 01:58:29', 261 | len: 8, 262 | step: 30, 263 | algo: 'sha1', 264 | result: '07081804' 265 | }, 266 | { 267 | date: '2005-03-18 01:58:31', 268 | len: 8, 269 | step: 30, 270 | algo: 'sha1', 271 | result: '14050471' 272 | }, 273 | { 274 | date: '2009-02-13 23:31:30', 275 | len: 8, 276 | step: 30, 277 | algo: 'sha1', 278 | result: '89005924' 279 | }, 280 | { 281 | date: '2033-05-18 03:33:20', 282 | len: 8, 283 | algo: 'sha1', 284 | step: 30, 285 | result: '69279037' 286 | }, 287 | { 288 | date: '2603-10-11 11:33:20', 289 | len: 8, 290 | algo: 'sha1', 291 | step: 30, 292 | result: '65353130' 293 | } 294 | ] 295 | ); 296 | 297 | // it('base32_to_hex', async () => { 298 | // const { base32_to_hex } = await db.one( 299 | // ` 300 | // SELECT totp.base32_to_hex( 301 | // 'OH3NUPO3WOGOZZQ4' 302 | // ) 303 | // ` 304 | // ); 305 | // expect(base32_to_hex).toEqual('71f6da3ddbb38cece61c'); 306 | // }); 307 | 308 | // it('base32_to_hex', async () => { 309 | // const { base32_to_hex } = await db.one( 310 | // ` 311 | // SELECT totp.base32_to_hex( 312 | // 'pv6624hvb4kdcwe2' 313 | // ) 314 | // ` 315 | // ); 316 | // expect(base32_to_hex).toEqual('7d7ded70f50f1431589a'); 317 | // }); 318 | 319 | cases( 320 | 'issue', 321 | async (opts) => { 322 | const { generate } = await db.one( 323 | ` 324 | SELECT totp.generate( 325 | secret := $1, 326 | period := $2, 327 | digits := $3, 328 | time_from := $4, 329 | hash := $5, 330 | encoding := $6 331 | ) 332 | `, 333 | [opts.secret, opts.step, opts.len, opts.date, opts.algo, opts.encoding] 334 | ); 335 | expect(generate).toEqual(opts.result); 336 | expect(generate).toMatchSnapshot(); 337 | }, 338 | [ 339 | { 340 | encoding: null, 341 | secret: 'OH3NUPO3WOGOZZQ4', 342 | date: '2020-11-14 07:46:37.212048+00', 343 | len: 6, 344 | step: 30, 345 | algo: 'sha1', 346 | result: '476240' 347 | }, 348 | { 349 | encoding: 'base32', 350 | secret: 'OH3NUPO3WOGOZZQ4', 351 | date: '2020-11-14 07:46:37.212048+00', 352 | len: 6, 353 | step: 30, 354 | algo: 'sha1', 355 | result: '788648' 356 | }, 357 | { 358 | encoding: 'base32', 359 | secret: 'OH3NUPO', 360 | date: '2020-11-14 07:46:37.212048+00', 361 | len: 6, 362 | step: 30, 363 | algo: 'sha1', 364 | result: '080176' 365 | } 366 | ] 367 | ); 368 | -------------------------------------------------------------------------------- /packages/totp/test/totp.test.js: -------------------------------------------------------------------------------- 1 | import { getConnections } from './utils'; 2 | 3 | let db, totp, teardown; 4 | const objs = { 5 | tables: {} 6 | }; 7 | 8 | beforeAll(async () => { 9 | ({ db, teardown } = await getConnections()); 10 | totp = db.helper('totp'); 11 | }); 12 | 13 | afterAll(async () => { 14 | try { 15 | //try catch here allows us to see the sql parsing issues! 16 | await teardown(); 17 | } catch (e) { 18 | // noop 19 | } 20 | }); 21 | 22 | beforeEach(async () => { 23 | await db.beforeEach(); 24 | }); 25 | 26 | afterEach(async () => { 27 | await db.afterEach(); 28 | }); 29 | 30 | it('generates secrets', async () => { 31 | const secrets = await db.one(`SELECT * FROM totp.random_base32($1)`, [16]); 32 | expect(secrets).toBeTruthy(); 33 | }); 34 | it('interval TOTP', async () => { 35 | const [ 36 | { interval } 37 | ] = await db.any(`SELECT * FROM totp.generate($1) as interval`, [ 38 | 'vmlhl2knm27eftq7' 39 | ]); 40 | // console.log('interval TOTP', interval); 41 | expect(interval).toBeTruthy(); 42 | }); 43 | it('TOTP', async () => { 44 | const [{ totp }] = await db.any( 45 | `SELECT * FROM totp.generate( 46 | secret := $1, 47 | period := $2, 48 | digits := $3, 49 | time_from := $4, 50 | encoding := 'base32' 51 | ) as totp`, 52 | ['vmlhl2knm27eftq7', 30, 6, '2020-02-05 22:11:40.56915+00'] 53 | ); 54 | expect(totp).toEqual('295485'); 55 | }); 56 | it('validation', async () => { 57 | const [{ verified }] = await db.any( 58 | `SELECT * FROM totp.verify( 59 | secret := $1, 60 | check_totp := $2, 61 | period := $3, 62 | digits := $4, 63 | time_from := $5, 64 | encoding := 'base32' 65 | ) as verified`, 66 | ['vmlhl2knm27eftq7', '295485', 30, 6, '2020-02-05 22:11:40.56915+00'] 67 | ); 68 | expect(verified).toBe(true); 69 | }); 70 | it('URL Encode', async () => { 71 | const [{ urlencode }] = await db.any( 72 | ` 73 | SELECT * FROM totp.urlencode($1) 74 | `, 75 | ['http://hu.wikipedia.org/wiki/São_Paulo'] 76 | ); 77 | expect(urlencode).toEqual('http://hu.wikipedia.org/wiki/S%C3%A3o_Paulo'); 78 | }); 79 | it('URLs', async () => { 80 | const [{ url }] = await db.any( 81 | ` 82 | SELECT * FROM totp.url($1, $2, $3, $4) as url 83 | `, 84 | ['dude@example.com', 'vmlhl2knm27eftq7', 30, 'acme'] 85 | ); 86 | expect(url).toEqual( 87 | 'otpauth://totp/dude@example.com?secret=vmlhl2knm27eftq7&period=30&issuer=acme' 88 | ); 89 | }); 90 | it('time-based validation wont verify in test', async () => { 91 | const [{ verified }] = await db.any( 92 | ` 93 | SELECT * FROM totp.verify($1, $2, $3, $4) as verified 94 | `, 95 | ['vmlhl2knm27eftq7', '843386', 30, 6] 96 | ); 97 | expect(verified).toBe(false); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/totp/test/utils/index.js: -------------------------------------------------------------------------------- 1 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 2 | export { getConnections } from '@launchql/db-testing'; 3 | -------------------------------------------------------------------------------- /packages/totp/verify/schemas/totp/procedures/generate_totp.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/totp/procedures/generate_totp on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_function ('totp.generate_totp'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /packages/totp/verify/schemas/totp/procedures/random_base32.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/totp/procedures/random_base32 on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_function ('totp.random_base32'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /packages/totp/verify/schemas/totp/procedures/urlencode.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/totp/procedures/urlencode on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_function ('totp.urlencode'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /packages/totp/verify/schemas/totp/schema.sql: -------------------------------------------------------------------------------- 1 | -- Verify schemas/totp/schema on pg 2 | 3 | BEGIN; 4 | 5 | SELECT verify_schema ('totp'); 6 | 7 | ROLLBACK; 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # totp [![Build Status](https://travis-ci.com/pyramation/totp.svg?branch=master)](https://travis-ci.com/pyramation/totp) 2 | 3 | TOTP implementation in pure PostgreSQL plpgsql 4 | 5 | This extension provides the HMAC Time-Based One-Time Password Algorithm (TOTP) as specfied in RFC 6238/4226 as pure plpgsql functions. 6 | 7 | # Usage 8 | 9 | ## totp.generate 10 | 11 | ```sql 12 | SELECT totp.generate('mysecret'); 13 | 14 | -- you can also specify totp_interval, and totp_length 15 | SELECT totp.generate('mysecret', 30, 6); 16 | ``` 17 | 18 | In this case, produces a TOTP code of length 6 19 | 20 | ``` 21 | 013438 22 | ``` 23 | 24 | ## totp.verify 25 | 26 | ```sql 27 | SELECT totp.verify('mysecret', '765430'); 28 | 29 | -- you can also specify totp_interval, and totp_length 30 | SELECT totp.verify('mysecret', '765430', 30, 6); 31 | ``` 32 | 33 | Depending on input, returns `TRUE/FALSE` 34 | 35 | ## totp.url 36 | 37 | ```sql 38 | -- totp.url ( email text, totp_secret text, totp_interval int, totp_issuer text ) 39 | SELECT totp.url( 40 | 'customer@email.com', 41 | 'mysecret', 42 | 30, 43 | 'Acme Inc' 44 | ); 45 | ``` 46 | 47 | Will produce a URL-encoded string 48 | 49 | ``` 50 | otpauth://totp/customer@email.com?secret=mysecret&period=30&issuer=Acme%20Inc 51 | ``` 52 | 53 | # caveats 54 | 55 | * Currently only supports `sha1` 56 | * Currently only supports 20 byte secrets 57 | 58 | pull requests welcome! 59 | 60 | # credits 61 | 62 | Thanks to 63 | 64 | https://tools.ietf.org/html/rfc6238 65 | 66 | https://www.youtube.com/watch?v=VOYxF12K1vE 67 | 68 | https://pgxn.org/dist/otp/ 69 | 70 | # Development 71 | 72 | ## start the postgres db process 73 | 74 | First you'll want to start the postgres docker (you can also just use `docker-compose up -d`): 75 | 76 | ```sh 77 | make up 78 | ``` 79 | 80 | ## install modules 81 | 82 | Install modules 83 | 84 | ```sh 85 | yarn install 86 | ``` 87 | 88 | ## install the Postgres extensions 89 | 90 | Now that the postgres process is running, install the extensions: 91 | 92 | ```sh 93 | make install 94 | ``` 95 | 96 | This basically `ssh`s into the postgres instance with the `packages/` folder mounted as a volume, and installs the bundled sql code as pgxn extensions. 97 | 98 | ## testing 99 | 100 | Testing will load all your latest sql changes and create fresh, populated databases for each sqitch module in `packages/`. 101 | 102 | ```sh 103 | yarn test:watch 104 | ``` 105 | 106 | ## building new modules 107 | 108 | Create a new folder in `packages/` 109 | 110 | ```sh 111 | lql init 112 | ``` 113 | 114 | Then, run a generator: 115 | 116 | ```sh 117 | lql generate 118 | ``` 119 | 120 | You can also add arguments if you already know what you want to do: 121 | 122 | ```sh 123 | lql generate schema --schema myschema 124 | lql generate table --schema myschema --table mytable 125 | ``` 126 | 127 | ## deploy code as extensions 128 | 129 | `cd` into `packages/`, and run `lql package`. This will make an sql file in `packages//sql/` used for `CREATE EXTENSION` calls to install your sqitch module as an extension. 130 | 131 | ## recursive deploy 132 | 133 | You can also deploy all modules utilizing versioning as sqtich modules. Remove `--createdb` if you already created your db: 134 | 135 | ```sh 136 | lql deploy awesome-db --yes --recursive --createdb 137 | ``` 138 | -------------------------------------------------------------------------------- /skitch.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*", 4 | "extensions/*" 5 | ] 6 | } --------------------------------------------------------------------------------