001/* 002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Thomas Roger 018 */ 019 020package org.nuxeo.wopi; 021 022import java.math.BigInteger; 023import java.nio.ByteBuffer; 024import java.nio.charset.StandardCharsets; 025import java.security.GeneralSecurityException; 026import java.security.KeyFactory; 027import java.security.PublicKey; 028import java.security.Signature; 029import java.security.spec.KeySpec; 030import java.security.spec.RSAPublicKeySpec; 031import java.time.Duration; 032import java.time.Instant; 033 034import javax.xml.bind.DatatypeConverter; 035 036import org.nuxeo.ecm.core.api.NuxeoException; 037 038/** 039 * Proof key helper class. 040 * <p> 041 * See <a href="https://wopi.readthedocs.io/en/latest/scenarios/proofkeys.html"></a>. 042 * <p> 043 * See <a href= 044 * "https://github.com/Microsoft/Office-Online-Test-Tools-and-Documentation/blob/master/samples/java/ProofKeyTester.java"></a> 045 * 046 * @since 10.3 047 */ 048public class ProofKeyHelper { 049 050 public static final String KEY_FACTORY_ALGORITHM = "RSA"; 051 052 public static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; 053 054 public static final long EPOCH_IN_TICKS = 621355968000000000L; // January 1, 1970 (start of Unix epoch) in "ticks" 055 056 private ProofKeyHelper() { 057 // helper class 058 } 059 060 public static PublicKey getPublicKey(String modulus, String exponent) { 061 BigInteger mod = new BigInteger(1, DatatypeConverter.parseBase64Binary(modulus)); 062 BigInteger exp = new BigInteger(1, DatatypeConverter.parseBase64Binary(exponent)); 063 KeyFactory factory; 064 try { 065 factory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); 066 KeySpec ks = new RSAPublicKeySpec(mod, exp); 067 return factory.generatePublic(ks); 068 } catch (GeneralSecurityException e) { 069 throw new NuxeoException(e); 070 } 071 } 072 073 public static byte[] getExpectedProofBytes(String url, String accessToken, long timestamp) { 074 byte[] accessTokenBytes = accessToken.getBytes(StandardCharsets.UTF_8); 075 byte[] hostUrlBytes = url.toUpperCase().getBytes(StandardCharsets.UTF_8); 076 ByteBuffer byteBuffer = ByteBuffer.allocate(4 + accessTokenBytes.length + 4 + hostUrlBytes.length + 4 + 8); 077 byteBuffer.putInt(accessTokenBytes.length); 078 byteBuffer.put(accessTokenBytes); 079 byteBuffer.putInt(hostUrlBytes.length); 080 byteBuffer.put(hostUrlBytes); 081 byteBuffer.putInt(8); 082 byteBuffer.putLong(timestamp); 083 return byteBuffer.array(); 084 } 085 086 public static boolean verifyProofKey(PublicKey key, String proofKeyHeader, byte[] expectedProofBytes) { 087 try { 088 Signature verifier = Signature.getInstance(SIGNATURE_ALGORITHM); 089 verifier.initVerify(key); 090 verifier.update(expectedProofBytes); 091 byte[] signedProof = DatatypeConverter.parseBase64Binary(proofKeyHeader); 092 return verifier.verify(signedProof); 093 } catch (GeneralSecurityException e) { 094 return false; 095 } 096 } 097 098 /** 099 * Checks that the given {@code timestamp} is no more than 20 minutes old. 100 * 101 * @throws NuxeoException if the timestamp is older than 20 minutes 102 */ 103 public static boolean verifyTimestamp(long timestamp) { 104 long ticks = timestamp - EPOCH_IN_TICKS; // ticks 105 long ms = ticks / 10_000; // milliseconds 106 Instant instant = Instant.ofEpochMilli(ms); 107 Duration duration = Duration.between(instant, Instant.now()); 108 return duration.compareTo(Duration.ofMinutes(20)) <= 0; 109 } 110 111}